
// @preview-file on
import { React, toNode, useRef } from 'DoraX';
import {
  Body,
  BodyDef,
  BodyMoveType,
  Keyboard,
  KeyName,
  Sprite,
  Vec2,
  Rect,
  print,
  Color,
  Director,
  App,
  Content,
  Audio,
  DrawNode,
  View,
} from 'Dora';

// ===============================
// 1. 动画配置
// ===============================
type AnimState = 'idle' | 'run' | 'jump' | 'dash' | 'dead';

type AnimCfg = {
  file: string;
  frameWidth: number;
  frameHeight: number;
  frames: number;
  duration: number;
  loop: boolean;
  row: number;
  startCol: number;
};

// 正常体力动画（体力>50%）
const ANIM_CONFIG: Record<AnimState, AnimCfg> = {
  idle: {
    file: 'Image/role/idle.png',  // 正常体力-待机动画
    frameWidth: 48,
    frameHeight: 32,
    frames: 10,
    duration: 1,
    loop: true,
    row: 0,
    startCol: 0,
  },
  run: {
    file: 'Image/role/move.png',  // 正常体力-奔跑动画
    frameWidth: 48,
    frameHeight: 32,
    frames: 10,
    duration: 1,
    loop: true,
    row: 0,
    startCol: 0,
  },
  jump: {
    file: 'Image/role/jump.png',  // 正常体力-跳跃动画
    frameWidth: 48,
    frameHeight: 32,
    frames: 20,
    duration: 1.7,
    loop: false,
    row: 0,
    startCol: 0,
  },
  dash: {
    file: 'Image/role/dash.png',  // 正常体力-冲刺动画
    frameWidth: 48,
    frameHeight: 32,
    frames: 5,
    duration: 0.5,
    loop: false,
    row: 0,
    startCol: 0,
  },
  dead: {
    file: 'Image/role/Death1.png',  // 正常体力-死亡动画
    frameWidth: 48,
    frameHeight: 32,
    frames: 10,
    duration: 1.0,
    loop: false,
    row: 0,
    startCol: 0,
  },
};



// ===============================
// 2. 物理与游戏参数
// ===============================

// 移动相关参数
const MAX_MOVE_SPEED = 350;
const JUMP_SPEED = 600;
const GRAVITY_Y = -1200;

// 地面移动参数
const GROUND_ACCEL = 2500;
const GROUND_FRICTION = 2000;

// 空中移动参数
const AIR_ACCEL = 1500;
const AIR_FRICTION = 800;

// 冲刺/疾跑参数
const RUN_SPEED = 1200;
const SPRINT_DURATION = 0.15;
let SPRINT_COOLDOWN = 1.5;  // 可变，用于难度调节
const DASH_WINDUP_TIME = 0.06;
const DASH_WINDUP_SLOWDOWN = 0.15;
const DASH_END_SPEED = 350;

// 难度设置
type DifficultyLevel = 'easy' | 'normal' | 'hard';
let currentDifficulty: DifficultyLevel = 'normal';
const DIFFICULTY_COOLDOWNS: Record<DifficultyLevel, number> = {
  easy: 0.8,    // 简单：0.8秒冷却
  normal: 1.5,  // 普通：1.5秒冷却
  hard: 2.5,    // 困难：2.5秒冷却
};

// 切换难度
const cycleDifficulty = () => {
  if (currentDifficulty === 'easy') {
    currentDifficulty = 'normal';
  } else if (currentDifficulty === 'normal') {
    currentDifficulty = 'hard';
  } else {
    currentDifficulty = 'easy';
  }
  SPRINT_COOLDOWN = DIFFICULTY_COOLDOWNS[currentDifficulty];
  print(`🎮 [难度] 切换为 ${getDifficultyName()} (冲刺冷却: ${SPRINT_COOLDOWN}秒)`);
};

// 获取难度中文名
const getDifficultyName = () => {
  switch (currentDifficulty) {
    case 'easy': return '简单';
    case 'normal': return '普通';
    case 'hard': return '困难';
  }
};

// 墙壁相关参数
const WALL_CLIMB_SPEED = 800;
const WALL_SLIDE_SPEED = -150;
const WALL_JUMP_X = 450;
const WALL_JUMP_Y = 450;
const WALL_JUMP_CONTROL_LOCK = 0.2;

// 跳跃辅助参数
const JUMP_BUFFER_TIME = 0.1;
const COYOTE_TIME = 0.15;

// 玩家尺寸参数
const SCALE = 3;

// 相机震动参数
const SHAKE_DURATION = 0.2;
const SHAKE_INTENSITY = 15;

// 桥梁建造参数
const BRIDGE_MAX_LEN = 1000;
const BRIDGE_GROW_SPEED = 220;
const BRIDGE_THICKNESS = 6;
const BRIDGE_OFFSET_X_FRONT = 40;
const BRIDGE_OFFSET_Y = -30;

// 资源消耗参数
const BODY_RESOURCE_DRAIN = 1;
const CLIMB_DISABLE_AT = 0.5;
const JUMP_DECAY_START = 0.5;
const JUMP_MIN_SCALE = 0.2;
const DASH_DECAY_START = 0.5;
const DASH_MIN_SCALE = 0.3;

// 全局限制
const TOTAL_BRIDGE_LIMIT = 1000;

// 重力向量
const GRAVITY = Vec2(0, GRAVITY_Y);

// ===============================
// 音效配置
// ===============================
const SOUND_FILES = {
  jump: 'Audio/jump.wav',
  dash: 'Audio/dash.wav',
  bridge: 'Audio/bridge.wav',
  walk: 'Audio/jump.wav',
  bgm_menu: 'Audio/bgm_menu.mp3',      // 菜单音乐
  bgm_level_1_4: 'Audio/bgm_1_4.mp3',  // 1-4关音乐
  bgm_level_5_7: 'Audio/bgm_5_7.mp3',  // 5-7关音乐
  bgm_level_8: 'Audio/bgm_8.mp3',      // 第8关音乐
};

// 当前播放的BGM路径和ID
let currentBGM: string = '';
let currentBGMId: number = -1;

// 根据关卡获取对应的BGM
const getLevelBGM = (levelIndex: number): string => {
  if (levelIndex >= 0 && levelIndex <= 3) {
    return SOUND_FILES.bgm_level_1_4;  // 1-4关
  } else if (levelIndex >= 4 && levelIndex <= 6) {
    return SOUND_FILES.bgm_level_5_7;  // 5-7关
  } else if (levelIndex === 7) {
    return SOUND_FILES.bgm_level_8;    // 第8关
  }
  return SOUND_FILES.bgm_level_1_4;    // 默认
};

// 音效播放函数
const playSound = (soundFile: string, volume: number = 1.0) => {
  try {
    Audio.play(soundFile);
  } catch (e) {
    print(`⚠️ [音效] 播放失败: ${soundFile}, 错误: ${e}`);
  }
};

// 播放背景音乐（循环）
const playBGM = (soundFile: string, volume: number = 0.5) => {
  // 如果是同一首BGM，不重复播放
  if (currentBGM === soundFile) {
    return;
  }

  try {
    // 停止当前正在播放的BGM
    if (currentBGMId >= 0) {
      Audio.stop(currentBGMId);
      print(`🎵 [音乐] 停止旧BGM: ${currentBGM}`);
    }

    print(`🎵 [音乐] 切换BGM: ${soundFile}`);
    currentBGMId = Audio.play(soundFile, true);
    currentBGM = soundFile;
    print(`🎵 [音乐] 播放成功`);
  } catch (e) {
    print(`⚠️ [音乐] 播放失败: ${soundFile}, 错误: ${e}`);
  }
};

// ===============================
// 3. 全局状态
// ===============================
let currentLevel = 0;
let playerPosition = { x: 0, y: 0 };
let collectedItems: boolean[] = [false, false, false, false, false, false, false]; // 7个关卡的收集品状态
let unlockedEndings: { fake: boolean; sacrifice: boolean; perfect: boolean } = {
  fake: false,
  sacrifice: false,
  perfect: false,
};

// 获取已收集数量
const getCollectedCount = () => {
  return collectedItems.filter(c => c).length;
};

// 检查是否全部收集
const isAllCollected = () => {
  return collectedItems.every(c => c);
};

// 获取已解锁结局数量
const getUnlockedEndingsCount = () => {
  let count = 0;
  if (unlockedEndings.fake) count++;
  if (unlockedEndings.sacrifice) count++;
  if (unlockedEndings.perfect) count++;
  return count;
};

// ===============================
// 4. 存档系统
// ===============================
const SAVE_FILE = 'game_save.txt';

const loadSave = (): number => {
  const saveText = Content.load(SAVE_FILE);
  if (saveText && saveText.length > 0) {
    const parts = saveText.split(',');
    const unlockedLevels = parseInt(parts[0]);
    if (!isNaN(unlockedLevels)) {
      // 加载收藏品进度
      if (parts.length > 1) {
        for (let i = 0; i < 7; i++) {
          collectedItems[i] = parts[i + 1] === '1';
        }
      }
      // 加载结局解锁状态
      if (parts.length > 8) {
        unlockedEndings.fake = parts[8] === '1';
        unlockedEndings.sacrifice = parts[9] === '1';
        unlockedEndings.perfect = parts[10] === '1';
      }
      print(`📂 [存档] 加载成功: 已解锁 ${unlockedLevels + 1} 关, 收藏品: ${getCollectedCount()}/7, 结局: ${getUnlockedEndingsCount()}/3`);
      return unlockedLevels;
    }
  }
  print(`📂 [存档] 使用默认数据: 已解锁 1 关`);
  return 0;
};

// 只获取关卡进度，不加载收藏品
const getUnlockedLevels = (): number => {
  const saveText = Content.load(SAVE_FILE);
  if (saveText && saveText.length > 0) {
    const parts = saveText.split(',');
    const unlockedLevels = parseInt(parts[0]);
    if (!isNaN(unlockedLevels)) {
      return unlockedLevels;
    }
  }
  return 0;
};

const saveSave = (unlockedLevels: number) => {
  // 保存格式: 关卡进度,收藏品1-7,结局1-3
  const collectibleData = collectedItems.map(c => c ? '1' : '0').join(',');
  const endingData = `${unlockedEndings.fake ? '1' : '0'},${unlockedEndings.sacrifice ? '1' : '0'},${unlockedEndings.perfect ? '1' : '0'}`;
  const saveText = `${unlockedLevels},${collectibleData},${endingData}`;
  const success = Content.save(SAVE_FILE, saveText);
  if (success) {
    print(`💾 [存档] 保存成功: 已解锁 ${unlockedLevels + 1} 关, 收藏品: ${getCollectedCount()}/7, 结局: ${getUnlockedEndingsCount()}/3`);
  } else {
    print(`❌ [存档] 保存失败`);
  }
};

const saveCollectibles = () => {
  const unlockedLevels = getUnlockedLevels();
  saveSave(unlockedLevels);
};

const unlockNextLevel = (currentLevelIndex: number) => {
  const unlockedLevels = getUnlockedLevels();
  if (currentLevelIndex >= unlockedLevels) {
    const newUnlockedLevels = currentLevelIndex + 1;
    saveSave(newUnlockedLevels);
    print(`🔓 [存档] 解锁第 ${newUnlockedLevels + 1} 关`);
  }
};

const isLevelUnlocked = (levelIndex: number): boolean => {
  const unlockedLevels = getUnlockedLevels();
  return levelIndex <= unlockedLevels;
};

const clearSave = () => {
  // 先重置收集品进度
  for (let i = 0; i < collectedItems.length; i++) {
    collectedItems[i] = false;
  }
  // 重置结局解锁状态
  unlockedEndings.fake = false;
  unlockedEndings.sacrifice = false;
  unlockedEndings.perfect = false;
  // 最后保存（这样保存的是重置后的数据）
  saveSave(0);
  print(`🗑️ [存档] 存档已清空，重置为第一关，收集品和结局进度已重置`);
};

// ===============================
// 5. 类型定义
// ===============================
type DialogMessage = {
  speaker: 'player' | 'npc';
  name: string;
  text: string;
};

type LevelDialog = {
  start: DialogMessage[];
  end: DialogMessage[];
};

type LevelConfig = {
  playerStart: { x: number; y: number };
  platforms: Array<{ x: number; y: number; w: number; h: number; file?: string }>;
  hazards: Array<{ x: number; y: number; w: number; h: number; file?: string }>;
  movingSpikes?: Array<{ startX: number; endX: number; y: number; width: number; height: number; speed: number; delay?: number; file?: string }>;
  goal: { x: number; y: number; size: number };
  secretGoal?: { x: number; y: number; size: number; targetLevel: number };
  collectible?: { x: number; y: number; file: string };
  // 关卡内文字提示
  texts?: Array<{
    x: number;
    y: number;
    text: string;
    fontSize?: number;
    color?: number;
    showDistance?: number;  // 玩家靠近多少距离时显示，0表示始终显示
  }>;
  npc: { x: number; y: number };
  dialog: LevelDialog;
  intro: {
    title: string;
    subtitle: string;
  };
  background: {
    file: string;
    width: number;
    height: number;
    parallax?: number;
    animated?: boolean;
    frameWidth?: number;
    frameHeight?: number;
    frames?: number;
    duration?: number;
  };
  // 摄像机X轴限制范围（相机坐标，可选）
  cameraLimitX?: { min: number; max: number };
  hidden?: boolean;
};

// ===============================
// 5. 辅助函数
// ===============================
const moveTowards = (current: number, target: number, maxDelta: number) => {
  if (Math.abs(target - current) <= maxDelta) return target;
  return current + Math.sign(target - current) * maxDelta;
};

const updateSpriteFrame = (sprite: Sprite.type, cfg: AnimCfg, time: number) => {
  if (!sprite || !sprite.texture) return;

  const tex = sprite.texture;
  const texW = tex.width;
  const texH = tex.height;
  if (texW <= 0 || texH <= 0) return;

  const fw = cfg.frameWidth;
  const fh = cfg.frameHeight;
  const cols = Math.floor(texW / fw);
  if (cols <= 0) return;

  let progress = time / cfg.duration;
  if (cfg.loop) {
    progress = progress % 1.0;
  } else {
    progress = Math.min(progress, 0.999);
  }

  const frameIndex = Math.floor(progress * cfg.frames);
  const globalIndex = cfg.startCol + frameIndex;
  const col = globalIndex % cols;
  const row = cfg.row + Math.floor(globalIndex / cols);

  const x = col * fw;
  const y = row * fh;

  sprite.textureRect = Rect(x, y, fw, fh);
  if (sprite.width !== fw) sprite.width = fw;
  if (sprite.height !== fh) sprite.height = fh;
};

// ===============================
// 6. Player 组件
// ===============================
interface PlayerProps {
  x: number;
  y: number;
  worldRef: any;
  debugWorldRef: any;  // 调试物理世界
  onDash: () => void;
  onDeath: () => void;
  onGoal: () => void;
  onSecretGoal?: () => void;
  levelConfig: LevelConfig;
  canMoveRef: any;
  triggerDeathRef?: any;  // 外部触发死亡
}

const Player = (props: PlayerProps) => {
  const bodyRef = useRef<Body.type>(null);
  const previewRef = useRef(null);
  const previewSpriteRef = useRef(null);
  const previewGlowRef = useRef(null);
  const idleRef = useRef<Sprite.type>(null);
  const runRef = useRef<Sprite.type>(null);
  const jumpRef = useRef<Sprite.type>(null);
  const dashRef = useRef<Sprite.type>(null);
  const deadRef = useRef<Sprite.type>(null);
  const currentAnimStateRef = useRef<AnimState>('idle');
  const animTimerRef = useRef(0);
  const lastDirectionRef = useRef<'left' | 'right'>('right');
  const sprintTimerRef = useRef(0);
  const sprintCooldownRef = useRef(0);
  const sprintVelRef = useRef(Vec2(0, 0));
  const lastSprintTimerRef = useRef(0);
  const dashWindupTimerRef = useRef(0);
  const dashWindupDirectionRef = useRef(Vec2(0, 0));
  const lastWindupTimerRef = useRef(0);
  const coyoteTimerRef = useRef(0);
  const jumpBufferTimerRef = useRef(0);
  const groundStabilityTimerRef = useRef(0);
  const lastSpacePressedRef = useRef(false);
  const wallJumpFromDirRef = useRef<0 | -1 | 1>(0);
  const wallJumpLockRef = useRef(0);
  const bridgeLenRef = useRef(0);
  const bodyResourceRef = useRef(1);
  const lastKPressedRef = useRef(false);
  const totalBridgeLenRef = useRef(0);
  const isDeadRef = useRef(false);
  const isWonRef = useRef(false);
  const bridgeStartXRef = useRef(0);
  const bridgeStartYRef = useRef(0);
  const hasAirDashedRef = useRef(false);
  const previewPulseRef = useRef(0);
  const walkSoundCooldownRef = useRef(0);  // 行走音效冷却
  const bridgesRef = useRef<Array<{body: any, debug: any, len: number}>>([]); // 桥梁调试数组

  const handleUpdate = (deltaTime: number) => {
    const body = bodyRef.current;
    const preview = previewRef.current;
    const previewSprite = previewSpriteRef.current;
    const previewGlow = previewGlowRef.current;
    const world = props.worldRef.current;
    const sIdle = idleRef.current;
    const sRun = runRef.current;
    const sJump = jumpRef.current;
    const sDash = dashRef.current;
    const sDead = deadRef.current;

    if (!body || !sIdle || !sRun || !sJump || !sDash || !sDead || !world || !preview) return false;

    // 同步所有桥梁调试绘制的位置和角度
    for (const b of bridgesRef.current) {
      if (b.body && b.debug) {
        b.debug.x = b.body.x;
        b.debug.y = b.body.y;
        b.debug.angle = b.body.angle;
      }
    }

    // 检测外部触发死亡（如移动尖刺）- 放在最前面，不受其他状态影响
    if (props.triggerDeathRef && props.triggerDeathRef.current && !isDeadRef.current) {
      print('💀 [Player] 被追逐尖刺击中，开始死亡动画');
      isDeadRef.current = true;
      props.triggerDeathRef.current = false;
    }

    if (isDeadRef.current) {
      body.velocityX = 0;
      body.velocityY = 0;

      if (currentAnimStateRef.current !== 'dead') {
        currentAnimStateRef.current = 'dead';
        animTimerRef.current = 0;
        sIdle.visible = false;
        sRun.visible = false;
        sJump.visible = false;
        sDash.visible = false;
        sDead.visible = true;
      }

      animTimerRef.current += deltaTime;
      const cfg = ANIM_CONFIG['dead'];
      updateSpriteFrame(sDead, cfg, animTimerRef.current);
      const facingScale = (lastDirectionRef.current === 'left' ? -1 : 1) * SCALE;
      sDead.scaleX = facingScale;
      sDead.scaleY = SCALE;

      if (animTimerRef.current >= cfg.duration) {
        print('✅ [Player] 死亡动画播放完成');
        props.onDeath();
      }

      return false;
    }

    if (isWonRef.current) {
      body.velocityX = 0;
      body.velocityY = 0;
      return false;
    }

    const currentCanMove = props.canMoveRef.current;
    if (!currentCanMove) {
      body.velocityX = 0;
      body.velocityY = 0;
      playerPosition.x = body.x;
      playerPosition.y = body.y;

      if (currentAnimStateRef.current !== 'idle') {
        currentAnimStateRef.current = 'idle';
        animTimerRef.current = 0;
        sIdle.visible = true;
        sRun.visible = false;
        sJump.visible = false;
        sDash.visible = false;
        sDead.visible = false;
      }

      animTimerRef.current += deltaTime;
      const cfg = ANIM_CONFIG['idle'];
      updateSpriteFrame(sIdle, cfg, animTimerRef.current);

      return false;
    }

    const playerX = body.x;
    const playerY = body.y;

    // 检测危险区域碰撞
    for (const hazard of props.levelConfig.hazards) {
      const dx = playerX - hazard.x;
      const dy = playerY - hazard.y;
      const distance = Math.sqrt(dx * dx + dy * dy);

      if (distance < 60) {
        if (!isDeadRef.current) {
          print('💀 [Player] 触碰危险区，开始死亡动画');
          isDeadRef.current = true;
        }
        return false;
      }
    }

    // 检测终点碰撞
    const goal = props.levelConfig.goal;
    const gdx = playerX - goal.x;
    const gdy = playerY - goal.y;
    const gDistance = Math.sqrt(gdx * gdx + gdy * gdy);

    if (gDistance < 60) {
      if (!isWonRef.current && !isDeadRef.current) {
        print('🎉 [Player] 到达终点');
        isWonRef.current = true;
        props.onGoal();
      }
      return false;
    }

    // 检测隐藏终点碰撞
    const secretGoal = props.levelConfig.secretGoal;
    if (secretGoal && props.onSecretGoal) {
      const sdx = playerX - secretGoal.x;
      const sdy = playerY - secretGoal.y;
      const sDistance = Math.sqrt(sdx * sdx + sdy * sdy);

      if (sDistance < 50) {
        if (!isWonRef.current && !isDeadRef.current) {
          print('🌟 [Player] 发现隐藏终点！');
          isWonRef.current = true;
          props.onSecretGoal();
        }
        return false;
      }
    }

    if (sprintCooldownRef.current > 0) sprintCooldownRef.current -= deltaTime;
    if (wallJumpLockRef.current > 0) wallJumpLockRef.current -= deltaTime;
    if (jumpBufferTimerRef.current > 0) jumpBufferTimerRef.current -= deltaTime;

    lastSprintTimerRef.current = sprintTimerRef.current;
    lastWindupTimerRef.current = dashWindupTimerRef.current;

    if (sprintTimerRef.current > 0) sprintTimerRef.current -= deltaTime;
    if (dashWindupTimerRef.current > 0) dashWindupTimerRef.current -= deltaTime;

    const isSpacePressed = Keyboard.isKeyPressed(KeyName.Space);
    if (Keyboard.isKeyDown(KeyName.Space)) {
      jumpBufferTimerRef.current = JUMP_BUFFER_TIME;
    }

    const currentVx = body.velocityX;
    const currentVy = body.velocityY;

    if (lastSpacePressedRef.current && !isSpacePressed) {
      if (currentVy > 0) body.velocityY = currentVy * 0.5;
    }
    lastSpacePressedRef.current = isSpacePressed;

    const kPressed = Keyboard.isKeyPressed(KeyName.K);
    const facingDir = lastDirectionRef.current === 'left' ? -1 : 1;
    const remainingBridgeLen = TOTAL_BRIDGE_LIMIT - totalBridgeLenRef.current;
    const canCreateMoreBridge = remainingBridgeLen > 10;

    // 更新预览脉动效果
    previewPulseRef.current += deltaTime * 4;

    if (kPressed && bodyResourceRef.current > 0.01 && canCreateMoreBridge) {
      if (!lastKPressedRef.current) {
        bridgeLenRef.current = 0;
        lastKPressedRef.current = true;
        print(`🌉 [桥梁] 开始建造`);
        playSound(SOUND_FILES.bridge, 0.5);  // 播放造桥音效
      }

      // 每帧更新桥梁起点，跟随玩家移动
      bridgeStartXRef.current = body.x + facingDir * BRIDGE_OFFSET_X_FRONT;
      bridgeStartYRef.current = body.y + BRIDGE_OFFSET_Y;

      const maxCurrentBridge = Math.min(BRIDGE_MAX_LEN, remainingBridgeLen);
      bridgeLenRef.current = Math.min(
        maxCurrentBridge,
        bridgeLenRef.current + BRIDGE_GROW_SPEED * deltaTime
      );

      bodyResourceRef.current = Math.max(
        0,
        bodyResourceRef.current -
          BODY_RESOURCE_DRAIN * deltaTime * (BRIDGE_GROW_SPEED / BRIDGE_MAX_LEN)
      );

      if (bridgeLenRef.current > 1) {
        preview.visible = true;
        preview.x = bridgeStartXRef.current;
        preview.y = bridgeStartYRef.current + bridgeLenRef.current * 0.5;
        preview.angle = 0; // 保持垂直

        // 更新预览视觉效果
        if (previewSprite) {
          previewSprite.scaleX = BRIDGE_THICKNESS;
          previewSprite.scaleY = bridgeLenRef.current;

          // 脉动透明度效果
          const pulseOpacity = 0.5 + Math.sin(previewPulseRef.current) * 0.2;
          previewSprite.opacity = pulseOpacity;

          // 根据剩余资源改变颜色
          const resourceRatio = bodyResourceRef.current;
         
        }

        // 更新发光效果
        if (previewGlow) {
          previewGlow.scaleX = BRIDGE_THICKNESS * 1.5;
          previewGlow.scaleY = bridgeLenRef.current;
          previewGlow.opacity = 0.3 + Math.sin(previewPulseRef.current * 1.5) * 0.15;
        }
      }
    } else if (!kPressed && lastKPressedRef.current) {
      const finalLength = bridgeLenRef.current;
      preview.visible = false;

      if (finalLength > 10) {
        const bridgeDef = BodyDef();
        bridgeDef.type = BodyMoveType.Dynamic;
        bridgeDef.linearAcceleration = GRAVITY;

        const mainLength = finalLength - BRIDGE_THICKNESS;
        if (mainLength > 0) {
          bridgeDef.attachPolygon(BRIDGE_THICKNESS, mainLength, 15000.0, 500.0, 0.0);
        }

        const topOffset = mainLength / 2;
        bridgeDef.attachDisk(Vec2(0, topOffset), BRIDGE_THICKNESS / 2, 15000.0, 500.0, 0.0);

        const bottomOffset = -mainLength / 2;
        bridgeDef.attachDisk(Vec2(0, bottomOffset), BRIDGE_THICKNESS / 2, 15000.0, 500.0, 0.0);

        const bridge = Body(
          bridgeDef,
          world,
          Vec2(bridgeStartXRef.current, bridgeStartYRef.current + finalLength * 0.5)
        );

        if (bridge) {
          print(`✅ [桥梁] 创建完成 - 位置: (${Math.floor(bridgeStartXRef.current)}, ${Math.floor(bridgeStartYRef.current + finalLength * 0.5)}), 长度: ${Math.floor(finalLength)}`);
          bridge.fixedRotation = false;
          bridge.linearDamping = 20.0;
          bridge.angularDamping = 20.0;
          bridge.angle = facingDir * 1;
          bridge.group = 2;  // 桥梁组，同组不碰撞

          // 在调试物理世界创建影子桥梁（只用于显示碰撞箱）
          const debugWorld = props.debugWorldRef.current;
          if (debugWorld) {
            const debugBridgeDef = BodyDef();
            debugBridgeDef.type = BodyMoveType.Static;
            if (mainLength > 0) {
              debugBridgeDef.attachPolygon(BRIDGE_THICKNESS, mainLength, 20000.0, 500.0, 0.0);
            }
            debugBridgeDef.attachDisk(Vec2(0, mainLength / 2), BRIDGE_THICKNESS / 2, 20000.0, 500.0, 0.0);
            debugBridgeDef.attachDisk(Vec2(0, -mainLength / 2), BRIDGE_THICKNESS / 2, 20000.0, 500.0, 0.0);

            const debugBridge = Body(
              debugBridgeDef,
              debugWorld,
              Vec2(bridge.x, bridge.y)
            );
            if (debugBridge) {
              debugBridge.angle = bridge.angle;
              bridgesRef.current.push({ body: bridge, debug: debugBridge, len: finalLength });
            }
          }

          totalBridgeLenRef.current += finalLength;
        }
      }

      bridgeLenRef.current = 0;
      lastKPressedRef.current = false;
    } else if (!kPressed) {
      // 不在建造时隐藏预览
      preview.visible = false;
    }

    const usedRatio = 1 - bodyResourceRef.current;

    if (Math.abs(currentVy) < 10) {
      groundStabilityTimerRef.current += deltaTime;
    } else {
      groundStabilityTimerRef.current = 0;
    }
    const isGrounded = groundStabilityTimerRef.current > 0.05;

    if (isGrounded) {
      coyoteTimerRef.current = COYOTE_TIME;
      wallJumpLockRef.current = 0;
      wallJumpFromDirRef.current = 0;
      hasAirDashedRef.current = false;
    } else if (coyoteTimerRef.current > 0) {
      coyoteTimerRef.current -= deltaTime;
    }

    let inputX = 0;
    if (Keyboard.isKeyPressed(KeyName.A)) inputX = -1;
    else if (Keyboard.isKeyPressed(KeyName.D)) inputX = 1;

    let onWallLeft = false;
    let onWallRight = false;
    if (!isGrounded && currentVy < 10) {
      if (inputX === -1 && Math.abs(currentVx) < 10) onWallLeft = true;
      if (inputX === 1 && Math.abs(currentVx) < 10) onWallRight = true;
    }

    const climbDisabled = usedRatio >= CLIMB_DISABLE_AT;
    if (climbDisabled && (onWallLeft || onWallRight)) {
      onWallLeft = false;
      onWallRight = false;
    }

    let effectiveInputX = inputX;
    if (wallJumpLockRef.current > 0) {
      if ((wallJumpFromDirRef.current === -1 && inputX === -1) ||
          (wallJumpFromDirRef.current === 1 && inputX === 1)) {
        effectiveInputX = 0;
      }
    }

    const targetVx = effectiveInputX * MAX_MOVE_SPEED;
    const acceleration = isGrounded
      ? effectiveInputX !== 0 ? GROUND_ACCEL : GROUND_FRICTION
      : effectiveInputX !== 0 ? AIR_ACCEL : AIR_FRICTION;

    const nextVx = moveTowards(currentVx, targetVx, acceleration * deltaTime);

    const canSprintCooldown = sprintCooldownRef.current <= 0 && dashWindupTimerRef.current <= 0;
    const canSprintAir = isGrounded || !hasAirDashedRef.current;
    const canSprint = canSprintCooldown && canSprintAir;

    if (canSprint && Keyboard.isKeyDown(KeyName.J)) {
      let dirX = 0;
      let dirY = 0;
      if (Keyboard.isKeyPressed(KeyName.A)) dirX = -1;
      if (Keyboard.isKeyPressed(KeyName.D)) dirX = 1;
      if (Keyboard.isKeyPressed(KeyName.W)) dirY = 1;
      if (Keyboard.isKeyPressed(KeyName.S)) dirY = -1;

      // 禁用纯垂直向上冲刺，但允许斜向上（左上、右上）
      if (dirX === 0 && dirY > 0) {
        dirY = 0;  // 只按W时，禁用向上
      }

      if (dirX === 0 && dirY === 0) {
        dirX = lastDirectionRef.current === 'left' ? -1 : 1;
      }

      const len = Math.sqrt(dirX * dirX + dirY * dirY);
      const nx = len > 0 ? dirX / len : 0;
      const ny = len > 0 ? dirY / len : 0;

      dashWindupDirectionRef.current = Vec2(nx, ny);
      dashWindupTimerRef.current = DASH_WINDUP_TIME;

      print('🔵 [冲刺] 开始蓄力');
      props.onDash();
    }

    const justFinishedWindup = lastWindupTimerRef.current > 0 && dashWindupTimerRef.current <= 0;
    if (justFinishedWindup) {
      const nx = dashWindupDirectionRef.current.x;
      const ny = dashWindupDirectionRef.current.y;

      let dashScale = 1;
      if (usedRatio >= DASH_DECAY_START) {
        const t = (usedRatio - DASH_DECAY_START) / (1 - DASH_DECAY_START);
        dashScale = Math.max(DASH_MIN_SCALE, 1 - t * 0.7);
      }
			if (currentLevel === 7) {
        dashScale *= 0.3;
      }

      sprintVelRef.current = Vec2(nx * RUN_SPEED * dashScale, ny * RUN_SPEED * dashScale);
      sprintTimerRef.current = SPRINT_DURATION;
      sprintCooldownRef.current = SPRINT_COOLDOWN;
      wallJumpLockRef.current = 0;

      if (!isGrounded) {
        hasAirDashedRef.current = true;
        print('⚡ [冲刺] 执行空中冲刺（已使用空中冲刺次数）');
      } else {
        print('⚡ [冲刺] 执行地面冲刺');
      }

      playSound(SOUND_FILES.dash, 0.7);  // 播放冲刺音效
      props.onDash();
    }

    const isSprinting = sprintTimerRef.current > 0;
    const isWindingUp = dashWindupTimerRef.current > 0;

    const justFinishedSprint = lastSprintTimerRef.current > 0 && sprintTimerRef.current <= 0;

    if (isSprinting) {
      body.velocityX = sprintVelRef.current.x;
      body.velocityY = sprintVelRef.current.y;

      const intentX = sprintVelRef.current.x;
      const isHorizontalDash = Math.abs(intentX) > 10;

      if (isHorizontalDash) {
        const hitRightWall = intentX > 0 && currentVx < 50;
        const hitLeftWall = intentX < 0 && currentVx > -50;
        if (hitRightWall || hitLeftWall) {
          body.velocityY = WALL_CLIMB_SPEED;
        }
      }
    } else if (justFinishedSprint) {
      const sprintVel = sprintVelRef.current;
      const len = Math.sqrt(sprintVel.x * sprintVel.x + sprintVel.y * sprintVel.y);
      if (len > 0) {
        const nx = sprintVel.x / len;
        const ny = sprintVel.y / len;
        body.velocityX = nx * DASH_END_SPEED;
        body.velocityY = ny * DASH_END_SPEED;
      } else {
        body.velocityX = 0;
        body.velocityY = 0;
      }
      print('🛑 [冲刺] 冲刺结束，保持350速度');
    } else if (isWindingUp) {
      body.velocityX = currentVx * DASH_WINDUP_SLOWDOWN;
      body.velocityY = currentVy * DASH_WINDUP_SLOWDOWN;
    } else {
      body.velocityX = nextVx;
      if ((onWallLeft || onWallRight) && currentVy < 0 && currentVy < WALL_SLIDE_SPEED) {
        body.velocityY = WALL_SLIDE_SPEED;
      }
    }

    if (jumpBufferTimerRef.current > 0) {
      let jumped = false;
      let jumpScale = 1;
			
      if (usedRatio >= JUMP_DECAY_START) {
        const t = (usedRatio - JUMP_DECAY_START) / (1 - JUMP_DECAY_START);
        jumpScale = Math.max(JUMP_MIN_SCALE, 1 - t);
				
      }
      // 第八关（索引7）跳跃高度为原来的0.3
      if (currentLevel === 7) {
        jumpScale *= 0.3;
				
      }

      if (onWallLeft) {
        body.velocityX = WALL_JUMP_X;
        body.velocityY = WALL_JUMP_Y * jumpScale;
        jumped = true;
        wallJumpFromDirRef.current = -1;
        wallJumpLockRef.current = WALL_JUMP_CONTROL_LOCK;
        hasAirDashedRef.current = false;
      } else if (onWallRight) {
        body.velocityX = -WALL_JUMP_X;
        body.velocityY = WALL_JUMP_Y * jumpScale;
        jumped = true;
        wallJumpFromDirRef.current = 1;
        wallJumpLockRef.current = WALL_JUMP_CONTROL_LOCK;
        hasAirDashedRef.current = false;
      } else if (coyoteTimerRef.current > 0) {
        body.velocityY = JUMP_SPEED * jumpScale;
        jumped = true;
      }

      if (jumped) {
        if (!isSpacePressed) body.velocityY *= 0.5;
        sprintTimerRef.current = 0;
        coyoteTimerRef.current = 0;
        jumpBufferTimerRef.current = 0;
        groundStabilityTimerRef.current = 0;
        playSound(SOUND_FILES.jump, 0.6);  // 播放跳跃音效
      }
    }

    if (Math.abs(body.velocityX) > 10) {
      lastDirectionRef.current = body.velocityX < 0 ? 'left' : 'right';
    }
    const facingScale = (lastDirectionRef.current === 'left' ? -1 : 1) * SCALE;

    let targetState: AnimState = 'idle';
    if (isSprinting || isWindingUp) targetState = 'dash';
    else if (!isGrounded) targetState = 'jump';
    else if (Math.abs(body.velocityX) > 10) targetState = 'run';

    if (currentAnimStateRef.current !== targetState) {
      currentAnimStateRef.current = targetState;
      animTimerRef.current = 0;
      sIdle.visible = targetState === 'idle';
      sRun.visible = targetState === 'run';
      sJump.visible = targetState === 'jump';
      sDash.visible = targetState === 'dash';
      sDead.visible = false;
    }

    animTimerRef.current += deltaTime;
    const cfg = ANIM_CONFIG[targetState];

    let activeSprite = sIdle;
    if (targetState === 'run') activeSprite = sRun;
    else if (targetState === 'jump') activeSprite = sJump;
    else if (targetState === 'dash') activeSprite = sDash;

    updateSpriteFrame(activeSprite, cfg, animTimerRef.current);
    activeSprite.scaleX = facingScale;
    activeSprite.scaleY = SCALE;

    // 行走音效（带冷却）
    if (walkSoundCooldownRef.current > 0) {
      walkSoundCooldownRef.current -= deltaTime;
    }
    if (targetState === 'run' && isGrounded && walkSoundCooldownRef.current <= 0) {
      playSound(SOUND_FILES.walk, 0.3);
      walkSoundCooldownRef.current = 0.3;  // 0.3秒冷却
    }

    // 透明度随桥梁使用量线性变化（使用越多越透明）
    // bodyResourceRef.current: 1 = 满体力（未使用）, 0 = 无体力（全部使用）
    // opacity: 1.0~0.1 线性变化
    const newOpacity = 0.1 + bodyResourceRef.current * 0.9;

    // 冲刺冷却时添加白色滤镜
    const isOnCooldown = sprintCooldownRef.current > 0;
    const spriteColor = isOnCooldown ? Color(0x01FF0000) : Color(0xffffffff);

    // 先设置颜色
    sIdle.color = spriteColor;
    sRun.color = spriteColor;
    sJump.color = spriteColor;
    sDash.color = spriteColor;

    // 再设置透明度
    sIdle.opacity = newOpacity;
    sRun.opacity = newOpacity;
    sJump.opacity = newOpacity;
    sDash.opacity = newOpacity;

    playerPosition.x = body.x;
    playerPosition.y = body.y;

    return false;
  };

  return (
    <>
      <body ref={bodyRef} type={BodyMoveType.Dynamic} x={props.x} y={props.y} fixedRotation={true} group={2} onUpdate={handleUpdate}>
        <sprite ref={idleRef} file={ANIM_CONFIG.idle.file} scaleX={SCALE} scaleY={SCALE} filter="Point" visible={true} anchorX={0.5} anchorY={0.60} />
        <sprite ref={runRef} file={ANIM_CONFIG.run.file} scaleX={SCALE} scaleY={SCALE} filter="Point" visible={false} anchorX={0.5} anchorY={0.60} />
        <sprite ref={jumpRef} file={ANIM_CONFIG.jump.file} scaleX={SCALE} scaleY={SCALE} filter="Point" visible={false} anchorX={0.5} anchorY={0.60} />
        <sprite ref={dashRef} file={ANIM_CONFIG.dash.file} scaleX={SCALE} scaleY={SCALE} filter="Point" visible={false} anchorX={0.5} anchorY={0.60} />
        <sprite ref={deadRef} file={ANIM_CONFIG.dead.file} scaleX={SCALE} scaleY={SCALE} filter="Point" visible={false} anchorX={0.5} anchorY={0.60} />
        <rect-fixture width={15 * SCALE} height={25 * SCALE} friction={0.0} restitution={0.0}  density={5000.0}/>
      </body>
      <body ref={previewRef} type={BodyMoveType.Static} x={props.x} y={props.y} visible={false} order={100}>
        {/* 发光层 - 在底层 */}
        
        {/* 主桥梁预览 - 在上层 */}
        <sprite
          ref={previewSpriteRef}
          file={'Image/bridge.png'}  // 桥梁预览贴图
          width={1}
          height={1}
          filter="Point"
          opacity={0.7}
        />
      </body>
    </>
  );
};

// ===============================
// 7. 背景动画组件
// ===============================

interface AnimatedBackgroundProps {
  file: string;
  width: number;
  height: number;
  frameWidth: number;
  frameHeight: number;
  frames: number;
  duration: number;
  loop: boolean;
}

const AnimatedBackground = (props: AnimatedBackgroundProps) => {
  const spriteRef = useRef<Sprite.type>(null);
  const animTimerRef = useRef(0);

  const handleUpdate = (deltaTime: number) => {
    const sprite = spriteRef.current;
    if (!sprite || !sprite.texture) return false;

    animTimerRef.current += deltaTime;

    // 使用和角色动画相同的逻辑
    const tex = sprite.texture;
    const texW = tex.width;
    const texH = tex.height;
    if (texW <= 0 || texH <= 0) return false;

    const fw = props.frameWidth;
    const fh = props.frameHeight;
    const cols = Math.floor(texW / fw);
    if (cols <= 0) return false;

    let progress = animTimerRef.current / props.duration;
    if (props.loop) {
      progress = progress % 1.0;
    } else {
      progress = Math.min(progress, 0.999);
    }

    const frameIndex = Math.floor(progress * props.frames);
    const col = frameIndex % cols;
    const row = Math.floor(frameIndex / cols);

    const x = col * fw;
    const y = row * fh;

    sprite.textureRect = Rect(x, y, fw, fh);
    // 使用配置的显示大小，而不是帧大小
    if (sprite.width !== props.width) sprite.width = props.width;
    if (sprite.height !== props.height) sprite.height = props.height;

    return false;
  };

  return (
    <sprite
      ref={spriteRef}
      file={props.file}
      width={props.width}
      height={props.height}
      filter="Point"
      onUpdate={handleUpdate}
    />
  );
};

// ===============================
// 8. 场景物体组件
// ===============================

const Platform = (props: { x: number; y: number; w: number; h: number; file?: string }) => (
  <body type={BodyMoveType.Static} x={props.x} y={props.y} group={2}>
    <rect-fixture
      width={props.w}
      height={props.h}
      friction={50.0}
      restitution={0.0}
    />
    <sprite
      file={props.file || 'Image/ground.png'}  // 平台/地面贴图
      width={props.w}
      height={props.h}
      filter="Point"
    />
  </body>
);

const Hazard = (props: { x: number; y: number; w: number; h: number; file?: string }) => (
  <body type={BodyMoveType.Static} x={props.x} y={props.y} order={10} group={2}>
    <rect-fixture width={props.w} height={props.h} />
    <sprite file={props.file || 'Image/hazard.png'} width={props.w} height={props.h} filter="Point" />
  </body>
);

// 移动尖刺组件（追逐战用）
interface MovingSpikeProps {
  startX: number;      // 起始X位置
  endX: number;        // 终点X位置
  y: number;           // Y位置
  width: number;       // 宽度
  height: number;      // 高度
  speed: number;       // 移动速度
  delay?: number;      // 延迟开始时间（秒）
  file?: string;       // 自定义贴图
  onPlayerHit?: () => void;  // 玩家被击中回调
}

const MovingSpike = (props: MovingSpikeProps) => {
  const posXRef = useRef(props.startX);
  const bodyRef = useRef(null);
  const delayTimerRef = useRef(props.delay || 0);
  const startedRef = useRef(false);

  const handleUpdate = (deltaTime: number) => {
    const body = bodyRef.current;
    if (!body) return false;

    // 延迟启动
    if (delayTimerRef.current > 0) {
      delayTimerRef.current -= deltaTime;
      return false;
    }

    if (!startedRef.current) {
      startedRef.current = true;
      print(`⚠️ [追逐尖刺] 开始移动！`);
    }

    // 向右移动
    posXRef.current += props.speed * deltaTime;
    body.x = posXRef.current;

    // 检测玩家碰撞 - 增大检测范围，持续检测
    const dx = playerPosition.x - posXRef.current;
    const dy = playerPosition.y - props.y;

    // 碰撞检测（矩形）- 增大范围确保能检测到移动中的玩家
    if (Math.abs(dx) < props.width / 2 + 60 && Math.abs(dy) < props.height / 2 + 50) {
      if (props.onPlayerHit) {
        props.onPlayerHit();
      }
    }

    // 到达终点后停止
    if (posXRef.current >= props.endX) {
      posXRef.current = props.endX;
    }

    return false;
  };

  return (
    <body ref={bodyRef} type={BodyMoveType.Static} x={props.startX} y={props.y} onUpdate={handleUpdate}>
      <rect-fixture width={props.width} height={props.height} />
      <sprite
        file={props.file || 'Image/hazard.png'}
        width={props.width}
        height={props.height}
        filter="Point"
      />
    </body>
  );
};

const Goal = (props: { x: number; y: number; size: number }) => {
  const rotationRef = useRef(0);
  const nodeRef = useRef(null);

  const handleUpdate = (deltaTime: number) => {
    const node = nodeRef.current;
    if (!node) return false;
    rotationRef.current += deltaTime * 90;
    node.angle = rotationRef.current;
    return false;
  };

  return (
    <body type={BodyMoveType.Static} x={props.x} y={props.y}>
      <rect-fixture width={props.size} height={props.size} />
      <node ref={nodeRef} onUpdate={handleUpdate}>
        <sprite
          file={'Image/goal_star.png'}  // 终点星星贴图
          width={props.size}
          height={props.size}
          filter="Point"
        />
      </node>
    </body>
  );
};

const SecretGoal = (props: { x: number; y: number; size: number }) => {
  const rotationRef = useRef(0);
  const pulseRef = useRef(0);
  const nodeRef = useRef(null);
  const glowRef = useRef(null);

  const handleUpdate = (deltaTime: number) => {
    const node = nodeRef.current;
    const glow = glowRef.current;
    if (!node) return false;

    rotationRef.current -= deltaTime * 120;
    pulseRef.current += deltaTime * 3;

    node.angle = rotationRef.current;

    if (glow) {
      const scale = 1 + Math.sin(pulseRef.current) * 0.2;
      glow.scaleX = scale;
      glow.scaleY = scale;
      glow.opacity = 0.3 + Math.sin(pulseRef.current * 2) * 0.15;
    }

    return false;
  };

  return (
    <body type={BodyMoveType.Static} x={props.x} y={props.y}>
      <rect-fixture width={props.size} height={props.size} />
      <sprite
        ref={glowRef}
        file={'Image/goal_star.png'}  // 隐藏终点发光效果
        width={props.size * 2}
        height={props.size * 2}
        filter="Point"
        opacity={0.3}
      />
      <node ref={nodeRef} onUpdate={handleUpdate}>
        <sprite
          file={'Image/goal_star.png'}  // 隐藏终点星星贴图
          width={props.size}
          height={props.size}
          filter="Point"
        />
      </node>
    </body>
  );
};

// NPC待机动画配置
const NPC_ANIM_CONFIG = {
  file: 'Image/NPC.png',  // NPC待机动画图片
  frameWidth: 32,
  frameHeight: 48,
  frames: 7,
  duration: 0.7,
  loop: true,
};

const NPC = (props: { x: number; y: number }) => {
  const bobRef = useRef(0);
  const nodeRef = useRef(null);
  const spriteRef = useRef<Sprite.type>(null);
  const animTimerRef = useRef(0);

  const handleUpdate = (deltaTime: number) => {
    const node = nodeRef.current;
    const sprite = spriteRef.current;
    if (!node) return false;

    // 上下浮动效果
    bobRef.current += deltaTime * 2;
    const bobOffset = Math.sin(bobRef.current) * 5;
    node.y = bobOffset;

    // 更新帧动画
    if (sprite && sprite.texture) {
      animTimerRef.current += deltaTime;
      const cfg = NPC_ANIM_CONFIG;

      const tex = sprite.texture;
      const texW = tex.width;
      const texH = tex.height;
      if (texW > 0 && texH > 0) {
        const cols = Math.floor(texW / cfg.frameWidth);
        if (cols > 0) {
          let progress = (animTimerRef.current / cfg.duration) % 1.0;
          const frameIndex = Math.floor(progress * cfg.frames);
          const col = frameIndex % cols;
          const row = Math.floor(frameIndex / cols);

          sprite.textureRect = Rect(col * cfg.frameWidth, row * cfg.frameHeight, cfg.frameWidth, cfg.frameHeight);
        }
      }
    }

    return false;
  };

  const NPC_SCALE = 2;

  return (
    <body type={BodyMoveType.Static} x={props.x} y={props.y}>
      <rect-fixture width={40} height={60} sensorOnly={true} />

      <node ref={nodeRef} onUpdate={handleUpdate}>
        <sprite
          ref={spriteRef}
          file={NPC_ANIM_CONFIG.file}  // NPC待机动画
          scaleX={0.45}
          scaleY={3}
          filter="Point"
        />

        <node y={35}>
          <draw-node>
            <polygon-shape
              verts={[
                Vec2(-5, 0),
                Vec2(5, 0),
                Vec2(0, 8)
              ]}
              fillColor={0xffffffff}
            />
          </draw-node>
        </node>
      </node>
    </body>
  );
};

// 收集品组件
const Collectible = (props: { x: number; y: number; levelIndex: number; file: string; onCollect: () => void }) => {
  const bobRef = useRef(0);
  const collectedRef = useRef(false);
  const spriteRef = useRef(null);
  const bodyRef = useRef(null);

  // 检查是否已经收集过
  const alreadyCollected = collectedItems[props.levelIndex];

  const handleUpdate = (deltaTime: number) => {
    const sprite = spriteRef.current;
    const body = bodyRef.current;
    if (!sprite || collectedRef.current || alreadyCollected) return false;

    bobRef.current += deltaTime * 3;

    const bobOffset = Math.sin(bobRef.current) * 8;
    sprite.y = bobOffset;

    // 脉动效果
    const scale = 1.0 + Math.sin(bobRef.current * 2) * 0.1;
    sprite.scaleX = scale;
    sprite.scaleY = scale;

    // 检测玩家碰撞
    const dx = playerPosition.x - props.x;
    const dy = playerPosition.y - props.y;
    const distance = Math.sqrt(dx * dx + dy * dy);

    if (distance < 50) {
      collectedRef.current = true;
      collectedItems[props.levelIndex] = true;
      print(`🌟 [收集品] 收集了第 ${props.levelIndex + 1} 关的收集品！(${getCollectedCount()}/7)`);

      // 隐藏收集品
      if (body) {
        body.visible = false;
      }

      props.onCollect();
      return true; // 停止更新
    }

    return false;
  };

  // 如果已收集则不显示
  if (alreadyCollected) {
    return null;
  }

  return (
    <body ref={bodyRef} type={BodyMoveType.Static} x={props.x} y={props.y} onUpdate={handleUpdate}>
      <sprite
        ref={spriteRef}
        file={props.file}
        width={40}
        height={40}
        filter="Point"
      />
    </body>
  );
};

// 关卡内文字提示组件
interface LevelTextProps {
  x: number;
  y: number;
  text: string;
  fontSize?: number;
  color?: number;
  showDistance?: number;
}

const LevelText = (props: LevelTextProps) => {
  const labelRef = useRef(null);
  const showDistance = props.showDistance || 0;  // 0 表示始终显示

  const handleUpdate = (deltaTime: number) => {
    const label = labelRef.current;
    if (!label) return false;

    if (showDistance > 0) {
      // 计算与玩家的距离
      const dx = playerPosition.x - props.x;
      const dy = playerPosition.y - props.y;
      const distance = Math.sqrt(dx * dx + dy * dy);

      // 根据距离调整透明度（渐入渐出效果）
      if (distance < showDistance) {
        const fadeRatio = 1 - (distance / showDistance);
        label.opacity = Math.min(1, fadeRatio * 1.5);
      } else {
        label.opacity = 0;
      }
    } else {
      label.opacity = 1;
    }

    return false;
  };

  return (
    <node x={props.x} y={props.y} onUpdate={handleUpdate}>
      <label
        ref={labelRef}
        fontName='sarasa-mono-sc-regular'
        fontSize={props.fontSize || 24}
        text={props.text}
        alignment='Center'
        opacity={showDistance > 0 ? 0 : 1}
      />
    </node>
  );
};

// ===============================
// 8. 关卡数据配置
// ===============================

const LEVELS: LevelConfig[] = [
  {
    playerStart: { x: -600, y: 50 },
		cameraLimitX: { min: -1000, max: 1000 },  
    platforms: [
      // 地面分成5段，总长4000
      { x: -1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
     
      { x: 390, y: 50, w: 300, h: 700, file: 'Image/platform2.png' },
      { x: 150, y: 200, w: 100, h: 20, file: 'Image/platform.png' },
      { x: 1200, y: 200, w: 160, h: 20, file: 'Image/platform.png' },
			{ x: 800, y: 400, w: 160, h: 20, file: 'Image/platform.png' },
      { x: -100, y: -100, w: 150, h: 190, file: 'Image/platform2.png' }
			
//墙壁
			{ x: -1900, y: 225, w: 220, h: 1200, file: 'Image/Leftwall.png' },
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/Rightwall.png'},
    ],
    hazards: [
      { x: -400, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
      { x: 600, y: -150, w: 100, h: 20 ,file:'Image/trapup.png' },
      { x: 700, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
      { x: 800, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
      { x: 900, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
      { x: 1000, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
      { x: 2000, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
    ],
   
    goal: { x: 1200, y: 230, size: 40 },
    collectible: { x: -500, y: 50, file: 'Image/bluegreen1.png' },  // 第1关收集品-青绿色记忆碎片
    texts: [
      { x: -600, y: -50, text: 'A/D 移动  空格 跳跃', fontSize: 20, showDistance: 200 },
			{ x: -500, y: 50, text: '这是钥匙...', fontSize: 20, showDistance: 200 },
			{ x: -400, y: -50, text: '小心陷阱！', fontSize: 20, showDistance: 200 },
      { x: -200, y: -50, text: '长按空格跳的更高', fontSize: 20, showDistance: 200 },
      { x: 0, y: 100, text: '按j键冲刺，配合方向键可选择冲刺方向', fontSize: 20, showDistance: 250 },
      { x: 130, y: -80, text: '跳起来也可以冲刺！', fontSize: 20, showDistance: 200 },
      { x: 130, y: -100, text: 'w+d+j可以斜向冲', fontSize: 20, showDistance: 200 },
      { x: 150, y: 300, text: '试试在右边的墙上按d+空格', fontSize: 20, showDistance: 200 },
      { x: 200, y: 400, text: '爬不上去就冲上去！', fontSize: 20, showDistance: 200 },
			 { x: 390, y: 550, text: '按k建造桥梁', fontSize: 20, showDistance: 200 },
			 { x: 390, y: 530, text: '建桥越多，你的能力就越少', fontSize: 20, showDistance: 200 },
			 { x: 1200, y: 300, text: '通往下一关', fontSize: 20, showDistance: 200 },
    ],
    npc: { x: -800, y: -80 },
    dialog: {
      start: [
        { speaker: 'player', name: '你', text: '终于找到你了。' },
				{ speaker: 'player', name: '你', text: '跟我走，我带你出去。' },
        { speaker: 'npc', name: 'ta', text: '你是谁......这是哪里？' },
        { speaker: 'npc', name: 'ta', text: '好冷...我想不起来...' },
				{ speaker: 'npc', name: 'ta', text: '...' },
				{ speaker: 'npc', name: 'ta', text: '...' },
       
      ],
      end: [],
    },
    intro: {
      title: '第一章',
      subtitle: '沉睡的记忆',
    },
    background: {
      file: 'Image/role/BlueSky.png',  // 第1关背景-蓝天动画
      width: 2640,
      height: 1490,
      parallax: 0.3,
      animated: true,
      frameWidth: 792,
      frameHeight: 447,
      frames: 9,
      duration: 1.8,
    },
  },

  {
    playerStart: { x: -800, y: 100 },
cameraLimitX: { min: -1000, max: 1000 },  
    platforms: [
      // 地面分成5段，总长4000
       // 地面分成5段，总长4000
      { x: -1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      // 地板下方遮挡层
    
      // 中间障碍物
      { x: -600, y: -100, w: 200, h: 200 , file: 'Image/platform2.png'},
      { x: 200, y: -100, w: 200, h: 400, file: 'Image/platform2.png' },
      { x: -300, y: 400, w: 100, h: 20, file: 'Image/platform.png' },
      { x: 400, y: 100, w: 200, h: 800, file: 'Image/platform2.png' },
      { x: 600, y: -100, w: 200, h: 200 , file: 'Image/platform2.png'},
    { x: 1040, y: 380, w:100 , h: 20 , file: 'Image/platform.png'},
      // 天花板分成5段
      
      // 天花板上方遮挡层
    
      // 左右两侧墙壁
     { x: -1900, y: 225, w: 220, h: 1200, file: 'Image/Leftwall.png' },
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/Rightwall.png'},
      
 
    ],
    hazards: [
      {x: -450, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
      { x: -350, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
      { x: -250, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
			{ x: -150, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
			{ x: -50, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
			{ x: 50, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
			//{x: 1800, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
			{x: 1700, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
{x: 1600, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
{x: 1500, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
{x: 1400, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
			{x: 1300, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
	
			 {x: 1200, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
			 {x: 1100, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
      { x: 1000, y: -150, w: 100, h: 20 , file:'Image/trapup.png'},
      { x: 900, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
			{ x: 800, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
			//{ x: 700, y: -150, w: 100, h: 20, file:'Image/trapup.png' },
			
			
			{ x: 1040, y: 400, w: 40, h: 20, file:'Image/trapup.png'}
    ],
		 
    goal: { x: 1600, y: 100, size: 40 },
    collectible: { x: -300, y: 450, file: 'Image/orange1.png' },  // 第2关收集品-橙色记忆碎片
    npc: { x: -1000, y: -80 },
    dialog: {
      start: [
        { speaker: 'npc', name: 'ta', text: '你...好熟悉的感觉...' },
				 { speaker: 'npc', name: 'ta', text: '有个声音...滴、滴、滴...一直响着...' },
        { speaker: 'player', name: '你', text: '你还记得那个声音？' },
        { speaker: 'npc', name: 'ta', text: '很规律...像心跳...在白色的地方...' },
				{ speaker: 'player', name: '你', text: '那是你还在的证明。' },
				{ speaker: 'npc', name: 'ta', text: '我还在吗？' },
				{ speaker: 'player', name: '你', text: '在。所以我能找到你。' },
				{ speaker: 'npc', name: 'ta', text: '...' },
			{ speaker: 'npc', name:' ta', text: '...' },
      ],
      end: [],
    },
    intro: {
      title: '第二章',
      subtitle: '遗失的声音',
    },
     background: {
      file: 'Image/role/BlueSky.png',  // 第1关背景-蓝天动画
      width: 2640,
      height: 1490,
      parallax: 0.3,
      animated: true,
      frameWidth: 792,
      frameHeight: 447,
      frames: 9,
      duration: 1.8,
    },
  },

  {
    playerStart: { x: -1100, y: 150 },
		cameraLimitX: { min: -1000, max: 1000 },  
    platforms: [
         // 地面分成5段，总长4000
         { x: -1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
   
      // 中间障碍物
     
      { x: 200, y: -50, w: 200, h: 300 , file: 'Image/platform2.png'},
      
			{ x: -600, y: 400, w: 100, h: 20, file: 'Image/platform.png' },
			{ x: -1000, y: 350, w: 100, h: 20, file: 'Image/platform.png'},
     
      { x: 600, y: -20, w: 150, h: 350 , file: 'Image/platform2.png'},
			{ x: 600, y: 500, w: 150, h: 300 , file: 'Image/platform1.png'},

			{ x: 30, y: -50, w: 150, h: 300 , file: 'Image/platform2.png'},
			{ x: 30, y: 350, w: 150, h: 300 , file: 'Image/platform1.png'},

			{ x: 1000, y: -50, w:400 , h: 300 , file: 'Image/platform2.png'},
      
      // 天花板分成5段
   
 
      // 天花板上方遮挡层
    
      // 左右两侧墙壁
        { x: -1900, y: 225, w: 220, h: 1200, file: 'Image/Leftwall.png' },
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/Rightwall.png'},
      
    ],
    hazards: [
      { x: -1000, y: -150, w: 100, h: 20 , file: 'Image/trapup.png'},
      { x: -800, y: -150, w: 100, h: 20 , file: 'Image/trapup.png'},
      { x: -600, y: -150, w: 100, h: 20 , file: 'Image/trapup.png'},
			 { x: 600, y: 160, w: 150, h: 20 , file: 'Image/trapup.png'},
			{ x: 600, y: 340, w: 150, h: 20, file: 'Image/trapdown.png' },
			{ x: 600, y: 660, w: 150, h: 20, file: 'Image/trapup.png' },
    ],
    goal: { x: -1500, y: 550, size: 40 },
    collectible: { x: 1000, y: 150, file: 'Image/blue1.png' },  // 第3关收集品-蓝色记忆碎片
    npc: { x: -1300, y: -80 },
    dialog: {
      start: [
        { speaker: 'npc', name: 'ta', text: '我的身体...好像越来越轻...' },
				{ speaker: 'npc', name: 'ta', text: '像是要飘走了...' },
        { speaker: 'player', name: '你', text: '我抓着你。不会让你飘走的。' },
        { speaker: 'npc', name: 'ta', text: '你一直在吗？' },
				{ speaker: 'player', name: '你', text: '每天都在。' },
				
				{ speaker: 'npc', name: 'ta', text: '多久了？' },
				{ speaker: 'player', name: '你', text: '不重要。你在就好' },
				{ speaker: 'npc', name: 'ta', text: '...' },
				{ speaker: 'npc', name: 'ta', text: '...' },
      ],
      end: [],
    },
    intro: {
      title: '第三章',
      subtitle: '高空跳跃',
    },
    background: {
      file: 'Image/role/BlueSky.png',  // 第1关背景-蓝天动画
      width: 2640,
      height: 1490,
      parallax: 0.3,
      animated: true,
      frameWidth: 792,
      frameHeight: 447,
      frames: 9,
      duration: 1.8,
    },
  },

  {
    playerStart: { x: -800, y: -150 },
		cameraLimitX: { min: -1000, max: 1000 },  
		 
    platforms: [
       // 地面分成5段，总长4000
      { x: -1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      // 地板下方遮挡层
   
      // 中间障碍物
     
      
			{ x: -800, y: 200, w: 200, h: 500 ,file: 'Image/platform2.png'},
			{ x: -450, y: 300, w: 200, h: 600 ,file: 'Image/platform2.png'},
      
			{ x: -100, y: 100, w: 100, h: 20, file: 'Image/platform.png' },
			{ x: 100, y:100 , w: 100, h: 20, file: 'Image/platform.png' },
			{ x: 300, y: 100, w: 100, h: 20, file: 'Image/platform.png' },
			{ x: 500, y: 100, w: 100, h: 20, file: 'Image/platform.png' },
			{ x: 700, y: 100, w: 100, h: 20, file: 'Image/platform.png' },
			{ x: 800, y: -40, w: 100, h: 300, file: 'Image/platform2.png' },
			{ x: 1500, y: 360, w: 200, h: 50, file: 'Image/platform.png' },
     
     { x: 630, y: 260, w: 70, h: 20,file: 'Image/platform.png'},
				{ x: 430, y: 260, w: 70, h: 20,file: 'Image/platform.png'},
				{ x: 230, y: 260, w: 70, h: 20,file: 'Image/platform.png'},
				{ x: 30, y: 260, w: 70, h: 20,file: 'Image/platform.png'},
				{ x: -170, y: 260, w: 70, h: 20,file: 'Image/platform.png'},

      // 天花板分成5段
    
      // 天花板上方遮挡层
    
      // 左右两侧墙壁
       { x: -1900, y: 225, w: 220, h: 1200, file: 'Image/Leftwall.png' },
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/Rightwall.png'},
      
    ],
    hazards: [
    
				{ x: 630, y: 250, w: 70, h: 20,file: 'Image/trapdown.png'}
				{ x: 430, y: 250, w: 70, h: 20,file: 'Image/trapdown.png'}
				{ x: 230, y: 250, w: 70, h: 20,file: 'Image/trapdown.png'}
				{ x: 30, y: 250, w: 70, h: 20,file: 'Image/trapdown.png'}
				{ x: -170, y: 250, w: 70, h: 20,file: 'Image/trapdown.png'}

				{ x: 630, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: 530, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 430, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: 330, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 230, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 130, y: -150, w: 100, h: 20, file: 'Image/trapup.png'}
				{ x: 30, y: -150, w: 100, h: 20, file: 'Image/trapup.png'}
				
    ],
    goal: { x: 1500, y: 460, size: 40 },
    collectible: { x: -800, y: 500, file: 'Image/pink1.png' },  // 第4关收集品-粉色记忆碎片
    npc: { x: -1000, y: -80 },
    dialog: {
      start: [
				{ speaker: 'npc', name: 'ta', text: '我想起来了...我推开过你...' },
				{ speaker: 'npc', name: 'ta', text: '我不想让你看见我...那个样子...' },
        { speaker: 'player', name: '你', text: '什么样子都是你。' },
        { speaker: 'npc', name: 'ta', text: '我以为离开你...你会轻松一些...' },
        { speaker: 'npc', name: 'ta', text: '对不起...' },
				{ speaker: 'player', name: '你', text: '别说了，走吧。' },
				{ speaker: 'npc', name: 'ta', text: '...' },
				{ speaker: 'npc', name: 'ta', text: '...' },
      ],
      end: [],
    },
    intro: {
      title: '第四章',
      subtitle: '曲折迷宫',
    },
    background: {
      file: 'Image/bg2.png',  // 第4关背景
      width: 2880,
      height: 1800,
      parallax: 0.38,
    },
  },

  {
    playerStart: { x: -800, y: 50 },
		cameraLimitX: { min: -1000, max: 1000 },  
		texts: [
     { x: -800, y: 100, text: '别被后面的墙追上！', fontSize: 20, showDistance: 200 },
		 
		 ],
    platforms: [
        // 地面分成5段，总长4000
       { x: -1920, y: -390, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -390, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -390, w: 1920, h: 490, file: 'Image/ground.png' },
      // 地板下方遮挡层
    
      // 中间障碍物
     
      
			{ x: -800, y: -100, w: 150, h: 225 , file: 'Image/platform2.png'},
			{ x: -300, y: -50, w: 100, h: 20 , file: 'Image/platform.png'},
			
			{ x: 300, y: 400, w: 200, h: 20 , file: 'Image/platform.png'},
			{ x: 600, y: 0, w: 200, h: 20 , file: 'Image/platform.png'},
			{ x: 1000, y: -50, w: 200, h: 500 , file: 'Image/platform2.png'},
			
      
     
     
      
            // 天花板上方遮挡层
     { x: -1920, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      { x: 0, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      
      { x: 1920, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      // 左右两侧墙壁
      { x: -1900, y: 225, w: 220, h: 1200 , file: 'Image/LeftWall.png'},
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/RightWall.png'},
    ],
		 movingSpikes: [
      {
        startX: -1800,        // 从右边开始
        endX: 1000,        // 移动到左边停止
        y: 300,             // Y位置
        width: 800,          // 宽度
        height: 1000,        // 高度（一面墙）
        speed: 50,         // 移动速度
        delay: 7            // 延迟3秒开始
      }
    ],
    hazards: [
     { x: -630, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: -530, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -430, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: -330, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -230, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -130, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -30, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 70, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				 { x: 670, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: 570, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 470, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: 370, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 270, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 170, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 770, y: -140, w: 100, h: 20,file: 'Image/trapup.png'}
			
				


    ],
    goal: { x: 1700, y: 0, size: 40 },
    collectible: { x: 300, y: 500, file: 'Image/green1.png' },  // 第5关收集品-绿色记忆碎片
    npc: { x: 1600, y: -80 },
    dialog: {
      start: [
        { speaker: 'npc', name: 'ta', text: '有人在读书...我听见过...' },
        { speaker: 'player', name: '你', text: '你最喜欢的那本。我读了很多遍。' },
        { speaker: 'npc', name: 'ta', text: '我听见了...但说不了话...' },
				{ speaker: 'player', name: '你', text: '我知道。你的手动了一下。' },
				{ speaker: 'player', name: '你', text: '那天我高兴了很久。' },
				 { speaker: 'npc', name: 'ta', text: '...' },
				 { speaker: 'npc', name: 'ta', text: '...' },
      ],
      end: [],
    },
    intro: {
      title: '第五章',
      subtitle: '危险地带',
    },
     background: {
      file: 'Image/bg2.png',  // 第4关背景
      width: 3000,
      height: 1800,
      parallax: 0.38,
    },
  },

  {
    playerStart: { x: -800, y: 200 },
		cameraLimitX: { min: -1000, max: 1000 },  
    platforms: [
      // 地面分成5段，总长4000
        // 地面分成5段，总长4000
       { x: -1920, y: -440, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -440, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -440, w: 1920, h: 490, file: 'Image/ground.png' },
      // 地板下方遮挡层
    
      // 中间障碍物
     
      
			{ x: -800, y: -130, w: 200, h: 150, file: 'Image/platform2.png' },
			{ x: -550, y: 300, w: 200, h: 450, file: 'Image/platform2.png' },
			{ x: -200, y: 300, w: 200, h: 400 , file: 'Image/platform2.png'},

			{ x: -300, y: -50, w: 150, h: 20 , file: 'Image/platform.png' },
			{ x: 0, y: 0, w: 200, h: 20 , file: 'Image/platform.png' },
			{ x: 200, y: 0, w: 200, h: 20 , file: 'Image/platform.png' },
			
			{ x: 300, y: 400, w: 200, h: 50 , file: 'Image/platform.png'},
			{ x: 600, y: -100, w: 200, h: 200, file: 'Image/platform2.png' },
			{ x: 600, y: 300, w: 200, h: 200 , file: 'Image/platform1.png'},
			{ x: 1000, y: 50, w: 200, h: 500 , file: 'Image/platform2.png'},
			
      
     
     
     
 
      // 天花板上方遮挡层
     { x: -1920, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      { x: 0, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      
      { x: 1920, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      // 左右两侧墙壁
      { x: -1900, y: 225, w: 220, h: 1200 , file: 'Image/LeftWall.png'},
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/RightWall.png'},
    ],
		  movingSpikes: [
      {
        startX: -1800,        // 从右边开始
        endX: 1000,        // 移动到左边停止
        y: 300,             // Y位置
        width: 800,          // 宽度
        height: 1000,        // 高度（一面墙）
        speed: 50,         // 移动速度
        delay: 7            // 延迟3秒开始
      }
			]
    hazards: [
			{ x: 600, y: 10, w: 200, h: 20 ,file: 'Image/trapup.png'},
			{ x: 600, y: 400, w: 200, h: 20 ,file: 'Image/trapup.png'},
			{ x: 600, y: 190, w: 200, h: 20 ,file: 'Image/trapdown.png'},
			 { x: -630, y: -190, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: -530, y: -190, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -430, y: -190, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: -330, y: -190, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -230, y: -190, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -130, y: -190, w: 100, h: 20,file: 'Image/trapup.png'}
			
      
    ],
    goal: { x: 1700, y: 0, size: 40 },
    collectible: { x: -350, y: 500, file: 'Image/yellow1.png' },  // 第6关收集品-黄色记忆碎片
    npc: { x: 1500, y: -110 },
    dialog: {
      start: [
				 { speaker: 'npc', name: 'ta', text: '我想起来了...你在哭...' },
				  { speaker: 'npc', name: 'ta', text: '你抓着我的手...说不许走...' },
        { speaker: 'player', name: '你', text: '你听到了？' },
        { speaker: 'npc', name: 'ta', text: '我想回应你...但睁不开眼睛...' },
        { speaker: 'npc', name: 'ta', text: '我在这里等了很久...等你来找我...' },
				 { speaker: 'player', name: '你', text: '我来了。' },
				 { speaker: 'npc', name: 'ta', text: '嗯...这次我不会松手了。' },
				 { speaker: 'npc', name: 'ta', text: '...' },
				 { speaker: 'npc', name: 'ta', text: '...' },
      ],
      end: [],
    },
    intro: {
      title: '第六章',
      subtitle: '极限跳跃',
    },
      background: {
      file: 'Image/bg2.png',  // 第4关背景
      width: 2880,
      height: 1800,
      parallax: 0.38,
    },
  },

  {
    playerStart: { x: -800, y: 250 },
		cameraLimitX: { min: -1000, max: 1000 },  
		texts: [
      { x: -800, y: 100, text: '<--  ?', fontSize: 20, showDistance: 200 },
			{ x: -1000, y: 0, text: '<--  ?', fontSize: 20, showDistance: 200 },
		 { x: -1200, y: 0, text: '<--  ?', fontSize: 20, showDistance: 200 },
		 { x: -1200, y: -50, text: '这是一条不归路...除非...?', fontSize: 20, showDistance: 200 },
		  { x: 1600, y: 400, text: '这里...是终点...吗？', fontSize: 20, showDistance: 200 },
		 ],
    platforms: [
     // 地面分成5段，总长4000
      { x: -1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
    
      // 中间障碍物
     
      
			{ x: -800, y: -100, w: 200, h: 150, file: 'Image/platform2.png' },
			{ x: -300, y: 300, w: 200, h: 400, file: 'Image/platform2.png' },
			{ x: 100, y: 300, w: 200, h: 400 , file: 'Image/platform2.png'},
		

			{ x: 0, y: -50, w: 150, h: 20 , file: 'Image/platform.png'},
		{ x: 600, y: -20, w: 150, h: 350 , file: 'Image/platform2.png'},
			{ x: 600, y: 500, w: 150, h: 300 , file: 'Image/platform1.png'},

			
			
			
			
			{ x: 200, y: -50, w: 20, h: 300 , file: 'Image/platform2.png'},
			
			{ x: 1600, y: 50, w: 200, h: 500 , file: 'Image/platform2.png'},
			
      
     
     
     
           // 天花板上方遮挡层
     { x: -1920, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      { x: 0, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      
      { x: 1920, y: 1420, w: 1920, h: 480, file: 'Image/TopWall.png' },
      // 左右两侧墙壁
      { x: -1900, y: 225, w: 220, h: 1200 , file: 'Image/LeftWall.png'},
      { x: 1900, y: 225, w: 220, h: 1200 , file: 'Image/RightWall.png'},
    ],
		  movingSpikes: [
      {
        startX: -1800,        // 从右边开始
        endX: 1000,        // 移动到左边停止
        y: 300,             // Y位置
        width: 800,          // 宽度
        height: 1000,        // 高度（一面墙）
        speed: 50,         // 移动速度
        delay: 12            // 延迟3秒开始
      }
			]
    hazards: [
     
				
			 { x: -630, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: -530, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -430, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
					{ x: -330, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -230, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: -130, y: -150, w: 100, h: 20,file: 'Image/trapup.png'}
				{ x: 600, y: 160, w: 150, h: 20 , file: 'Image/trapup.png'},
				{ x: 600, y: 650, w: 150, h: 20 , file: 'Image/trapup.png'},
			{ x: 600, y: 340, w: 150, h: 20, file: 'Image/trapdown.png' },
    ],
    goal: { x: 1600, y: 550, size: 40 },
    secretGoal: { x: -1400, y: 0, size: 30, targetLevel: 7 },
    collectible: { x: -300, y: 550, file: 'Image/red1.png' },  // 第7关收集品-红色记忆碎片
    npc: { x: 100, y: 550 },
    dialog: {
      start: [
        { speaker: 'npc', name: 'ta', text: '走吧，一起回家。' },
        { speaker: 'player', name: '你', text: '嗯。' },
        { speaker: 'npc', name: 'ta', text: '你先走，我跟着你。' },
				{ speaker: 'player', name: '你', text: '一起。' },
				{ speaker: 'npc', name: 'ta', text: '好...一起...' },
				{ speaker: 'npc', name: 'ta', text: '...' },
				{ speaker: 'npc', name: 'ta', text: '...' },
				

      ],
      end: [],
    },
    intro: {
      title: '第七章',
      subtitle: '终极试炼',
    },
    background: {
      file: 'Image/role/BlueSky.png',  // 第1关背景-蓝天动画
      width: 2640,
      height: 1490,
      parallax: 0.3,
      animated: true,
      frameWidth: 792,
      frameHeight: 447,
      frames: 9,
      duration: 1.8,
    },
  },

  // 隐藏第八关
  {
    playerStart: { x: -900, y: 100 },
		cameraLimitX: { min: -500, max: 1000 }, 
		texts: [
      { x: -900, y: 0, text: '你失去了一切...除了...桥', fontSize: 20, showDistance: 200 },
		
		 ],
    platforms: [
      // 地面分成5段，总长5000
      // 地面分成5段，总长4000
        { x: -1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      { x: 0, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      
      { x: 1920, y: -400, w: 1920, h: 490, file: 'Image/ground.png' },
      // 地板下方遮挡层
     
      // 中间障碍物
     { x: 400, y: 150, w: 250, h: 20 , file: 'Image/platform.png'},
		  {x: -100, y: 50, w: 200, h: 20 , file: 'Image/platform.png'},
		 { x: -500, y: -50, w: 200, h: 20 , file: 'Image/platform.png'},
		 { x: -900, y: -150, w: 200, h: 20 , file: 'Image/platform.png'},
		 
		 
     
     
      
     
     
     
 
      // 天花板上方遮挡层
    
      // 左右两侧墙壁
      { x: -1100, y: 225, w: 200, h: 1200 , file: 'Image/LeftWall.png'},
      { x: 600, y: 225, w: 200, h: 1200 , file: 'Image/RightWall.png'},
    ],
    hazards: [
   
    ],
    goal: { x: 420, y:190, size: 40 },
    npc: { x: 320, y: 210 },
    dialog: {
      start: [
        { speaker: 'npc', name: 'ta', text: '这里是...？' },
        { speaker: 'player', name: '你', text: '找到所有记忆碎片...就能一起回去。' },
        { speaker: 'npc', name: 'ta', text: '......好。' },
				{ speaker: 'npc', name: 'ta', text: '......' },
				{ speaker: 'npc', name: 'ta', text: '......' },
      ],
      end: [],
    },
    intro: {
      title: '???',
      subtitle: '隐藏试炼',
    },
    background: {
      file: 'Image/role/BlueSky.png',  // 第1关背景-蓝天动画
      width: 2640,
      height: 1490,
      parallax: 0.3,
      animated: true,
      frameWidth: 792,
      frameHeight: 447,
      frames: 9,
      duration: 1.8,
    },
    hidden: true,
  },
];

// 获取可见关卡数量（排除隐藏关卡）
const getVisibleLevelCount = () => {
  return LEVELS.filter(level => !level.hidden).length;
};

// ===============================
// 9. LevelIntro 组件
// ===============================

// 结局对话类型
type EndingDialogMessage = {
  speaker: 'player' | 'npc';
  text: string;
};

// 结局对话数据
const ENDING_DIALOGS = {
  // 假结局 - 女主消失（第七关正常终点）
  fakeEnding: [
   
    { speaker: 'npc', text: '你走慢点...我有点累...' },
    { speaker: 'player', text: '你的手好凉。' },
    { speaker: 'npc', text: '没事...快到了...' },
    { speaker: 'player', text: '我抓不住你了。' },
    { speaker: 'npc', text: '我在...你往前走...' },
    { speaker: 'player', text: '你在骗我。' },
    { speaker: 'npc', text: '没有...我就在后面...一直都在...' },
		{ speaker: 'npc', text: '好好活下去...' },
  ] as EndingDialogMessage[],

  // 真结局 - 以命换命（未全收集）
  sacrificeEnding: [
    { speaker: 'npc', text: '我们到了...一起走吧。' },
    { speaker: 'player', text: '你先走。' },
    { speaker: 'npc', text: '你怎么不动？' },
    { speaker: 'player', text: '有人要留下。' },
    { speaker: 'npc', text: '...你骗我。' },
    { speaker: 'player', text: '我说带你回去。没骗你。' },
    { speaker: 'npc', text: '我不走。' },
    { speaker: 'player', text: '带着我的灵魂回去' },
    { speaker: 'player', text: '替我去看日出。' },
    { speaker: 'player', text: '走吧。别回头。' },
  ] as EndingDialogMessage[],

  // 完美结局 - 一起回家（全收集）
  perfectEnding: [
    { speaker: 'npc', text: '我们到了...一起走吧。' },
   { speaker: 'player', text: '走吧。一起去看日出。' },
	 { speaker: 'npc', text: '好，一起回家' },
    { speaker: 'player', text: '一起回家。' },
  ] as EndingDialogMessage[],
};

// 结局场景组件
interface EndingSceneProps {
  dialogType: 'fakeEnding' | 'sacrificeEnding' | 'perfectEnding';
  endingImages: string[];  // 改为图片数组，支持多张图片像PPT一样切换
  onComplete: () => void;
}

const EndingScene = (props: EndingSceneProps) => {
  const dialogIndexRef = useRef(0);
  const imageIndexRef = useRef(0);  // 当前显示的图片索引
  const phaseRef = useRef<'dialog' | 'image'>('dialog');
  const lastSpacePressedRef = useRef(false);
  const textLabelRef = useRef(null);
  const nameLabelRef = useRef(null);
  const avatarPlayerRef = useRef(null);
  const avatarNpcRef = useRef(null);
  const nameNodeRef = useRef(null);
  const avatarNodeRef = useRef(null);
  const dialogNodeRef = useRef(null);
  const imageNodeRef = useRef(null);
  // 预创建8张图片的ref
  const image0Ref = useRef(null);
  const image1Ref = useRef(null);
  const image2Ref = useRef(null);
  const image3Ref = useRef(null);
  const image4Ref = useRef(null);
  const image5Ref = useRef(null);
  const image6Ref = useRef(null);
  const image7Ref = useRef(null);

  const messages = ENDING_DIALOGS[props.dialogType];
  const images = props.endingImages;

  const hideAllImages = () => {
    if (image0Ref.current) image0Ref.current.visible = false;
    if (image1Ref.current) image1Ref.current.visible = false;
    if (image2Ref.current) image2Ref.current.visible = false;
    if (image3Ref.current) image3Ref.current.visible = false;
    if (image4Ref.current) image4Ref.current.visible = false;
    if (image5Ref.current) image5Ref.current.visible = false;
    if (image6Ref.current) image6Ref.current.visible = false;
    if (image7Ref.current) image7Ref.current.visible = false;
  };

  const showCurrentImage = () => {
    hideAllImages();
    const index = imageIndexRef.current;
    if (index === 0 && image0Ref.current) image0Ref.current.visible = true;
    else if (index === 1 && image1Ref.current) image1Ref.current.visible = true;
    else if (index === 2 && image2Ref.current) image2Ref.current.visible = true;
    else if (index === 3 && image3Ref.current) image3Ref.current.visible = true;
    else if (index === 4 && image4Ref.current) image4Ref.current.visible = true;
    else if (index === 5 && image5Ref.current) image5Ref.current.visible = true;
    else if (index === 6 && image6Ref.current) image6Ref.current.visible = true;
    else if (index === 7 && image7Ref.current) image7Ref.current.visible = true;
  };

  const updateDisplay = () => {
    const msg = messages[dialogIndexRef.current];
    if (!msg) return;

    const textLabel = textLabelRef.current;
    const nameLabel = nameLabelRef.current;
    const avatarPlayer = avatarPlayerRef.current;
    const avatarNpc = avatarNpcRef.current;
    const nameNode = nameNodeRef.current;
    const avatarNode = avatarNodeRef.current;

    if (textLabel) textLabel.text = msg.text;
    if (nameLabel) nameLabel.text = msg.speaker === 'player' ? '你' : 'ta';
    // 切换头像显示
    if (avatarPlayer) avatarPlayer.visible = msg.speaker === 'player';
    if (avatarNpc) avatarNpc.visible = msg.speaker !== 'player';
    if (nameNode) {
      nameNode.x = msg.speaker === 'player' ? -300 : 300;
    }
    if (avatarNode) {
      avatarNode.x = msg.speaker === 'player' ? -340 : 340;
    }
  };

  const handleUpdate = (deltaTime: number) => {
    const spacePressed = Keyboard.isKeyPressed(KeyName.Space);
    const spaceDown = !lastSpacePressedRef.current && spacePressed;
    lastSpacePressedRef.current = spacePressed;

    if (phaseRef.current === 'dialog') {
      if (spaceDown) {
        advanceDialog();
      }
    } else if (phaseRef.current === 'image') {
      if (spaceDown) {
        advanceImage();
      }
    }

    return false;
  };

  const advanceDialog = () => {
    dialogIndexRef.current++;
    if (dialogIndexRef.current >= messages.length) {
      // 进入图片阶段
      phaseRef.current = 'image';
      imageIndexRef.current = 0;
      const dialogNode = dialogNodeRef.current;
      const imageNode = imageNodeRef.current;
      if (dialogNode) dialogNode.visible = false;
      if (imageNode) imageNode.visible = true;
      showCurrentImage();
    } else {
      updateDisplay();
    }
  };

  const advanceImage = () => {
    imageIndexRef.current++;
    if (imageIndexRef.current >= images.length) {
      // 所有图片播放完毕
      props.onComplete();
    } else {
      // 显示下一张图片
      showCurrentImage();
    }
  };

  const handleTap = (touch: any) => {
    if (phaseRef.current === 'dialog') {
      advanceDialog();
    } else if (phaseRef.current === 'image') {
      advanceImage();
    }
    return true;
  };

  const currentMsg = messages[dialogIndexRef.current] || messages[0];
  const isPlayer = currentMsg.speaker === 'player';

  return (
    <node onUpdate={handleUpdate} order={3000} touchEnabled={true} onTapBegan={handleTap}>
      <draw-node>
        <rect-shape width={3000} height={3000} fillColor={0x000000ff} />
      </draw-node>

      {/* 对话部分 */}
      <node ref={dialogNodeRef} visible={true}>
        <node y={-200}>
          <sprite /*file={'Image/ui_panel.png'}*/ width={800} height={200} opacity={0.9} />  {/* 结局对话框面板背景 */}

          {/* 名字标签 */}
          <node ref={nameNodeRef} x={isPlayer ? -350 : 300} y={100}>
            <sprite /*file={'Image/ui_button.png'}*/ width={120} height={36} />  {/* 名字标签背景 */}
            <label
              ref={nameLabelRef}
              fontName='sarasa-mono-sc-regular'
              fontSize={22}
              text={isPlayer ? '你' : 'ta'}
              y={-5} 
              alignment='Center'
            />
          </node>

          {/* 头像 */}
          <node ref={avatarNodeRef} x={isPlayer ? -340 : 340} y={0}>
            <sprite
              ref={avatarPlayerRef}
              file={'Image/avatar_player.png'}
              width={100}
              height={100}
              filter="Point"
              visible={isPlayer}
            />
            <sprite
              ref={avatarNpcRef}
              file={'Image/avatar_npc.png'}
              width={100}
              height={100}
              filter="Point"
              visible={!isPlayer}
            />
          </node>

          {/* 对话文本 */}
          <label
            ref={textLabelRef}
            fontName='sarasa-mono-sc-regular'
            fontSize={28}
            text={currentMsg.text}
            width={500}
            y={0}
            alignment='Center'
          />

          <node y={-80}>
            <label
              fontName='sarasa-mono-sc-regular'
              fontSize={18}
              text="按空格继续"
              alignment='Center'
              opacity={0.7}
            />
          </node>
        </node>
      </node>

      {/* 结局图片 - 预创建所有sprite，通过显示/隐藏切换 */}
      <node ref={imageNodeRef} visible={false}>
        {/* 纯黑色背景层 */}
        <node order={-10}>
          <draw-node>
            <rect-shape x={-1500} y={-1500} width={3000} height={3000} fillColor={0xff000000} />
          </draw-node>
        </node>
        <sprite ref={image0Ref} file={images[0] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image1Ref} file={images[1] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image2Ref} file={images[2] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image3Ref} file={images[3] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image4Ref} file={images[4] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image5Ref} file={images[5] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image6Ref} file={images[6] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={image7Ref} file={images[7] || 'Image/cg/LostEnding-1.png'} width={1000} height={1000} filter="Point" visible={false} />
        <node y={-350}>
          <label
            fontName='sarasa-mono-sc-regular'
            fontSize={18}
            text="按空格继续"
            alignment='Center'
            opacity={0.7}
          />
        </node>
      </node>
    </node>
  );
};

// 过渡动画组件（用于第七关结束和第八关结束）
interface TransitionAnimProps {
  title: string;
  subtitle: string;
  onComplete: () => void;
  animFile?: string;
  animFrameWidth?: number;
  animFrameHeight?: number;
  animFrames?: number;
  animDuration?: number;
}

const TransitionAnim = (props: TransitionAnimProps) => {
  const timerRef = useRef(0);
  const completedRef = useRef(false);
  const spriteRef = useRef<Sprite.type>(null);

  const handleUpdate = (deltaTime: number) => {
    if (completedRef.current) return false;

    timerRef.current += deltaTime;

    // 更新动画帧
    if (props.animFile && spriteRef.current && spriteRef.current.texture) {
      const sprite = spriteRef.current;
      const tex = sprite.texture;
      const texW = tex.width;
      const texH = tex.height;

      if (texW > 0 && texH > 0) {
        const fw = props.animFrameWidth || texW;
        const fh = props.animFrameHeight || texH;
        const frames = props.animFrames || 1;
        const duration = props.animDuration || 3.0;
        const cols = Math.floor(texW / fw);

        if (cols > 0) {
          let progress = (timerRef.current / duration) % 1.0;
          const frameIndex = Math.floor(progress * frames);
          const col = frameIndex % cols;
          const row = Math.floor(frameIndex / cols);

          sprite.textureRect = Rect(col * fw, row * fh, fw, fh);
        }
      }
    }

    if (timerRef.current >= 3.0 && !completedRef.current) {
      completedRef.current = true;
      props.onComplete();
      return true;
    }

    return false;
  };

  const timer = timerRef.current;
  let fadeOpacity = 1.0;
  if (timer < 0.5) {
    fadeOpacity = timer / 0.5;
  } else if (timer > 2.5) {
    fadeOpacity = (3.0 - timer) / 0.5;
  }

  return (
    <node onUpdate={handleUpdate} order={3000}>
      <draw-node>
        <rect-shape width={2000} height={2000} fillColor={0x000000ee} />
      </draw-node>

      {/* 自定义动画 */}
      {props.animFile && (
        <sprite
          ref={spriteRef}
          file={props.animFile}
          filter="Point"
          opacity={fadeOpacity}
        />
      )}

      <node y={50} opacity={fadeOpacity}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={48}
          text={props.title}
          y={-12}
          alignment='Center'
        />
      </node>

      <node y={-20} opacity={fadeOpacity}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={28}
          text={props.subtitle}
          y={-7}
          alignment='Center'
        />
      </node>
    </node>
  );
};

interface LevelIntroProps {
  title: string;
  subtitle: string;
  onComplete: () => void;
}

const SnowParticle = (props: { initialX: number; initialY: number; speed: number; size: number }) => {
  const posRef = useRef({ x: props.initialX, y: props.initialY });
  const nodeRef = useRef(null);

  const handleUpdate = (deltaTime: number) => {
    const node = nodeRef.current;
    if (!node) return false;

    posRef.current.y -= props.speed * deltaTime;

    if (posRef.current.y < -600) {
      posRef.current.y = 600;
      posRef.current.x = (Math.random() - 0.5) * 1600;
    }

    node.x = posRef.current.x;
    node.y = posRef.current.y;

    return false;
  };

  return (
    <node ref={nodeRef} x={props.initialX} y={props.initialY} onUpdate={handleUpdate}>
      <draw-node>
        <dot-shape radius={props.size} color={0xffffffff} />
      </draw-node>
    </node>
  );
};

const LevelIntro = (props: LevelIntroProps) => {
  const timerRef = useRef(0);
  const completedRef = useRef(false);
  const lastSpacePressedRef = useRef(false);
  const snowParticlesRef = useRef<Array<{ x: number; y: number; speed: number; size: number }>>([]);

  if (snowParticlesRef.current.length === 0) {
    for (let i = 0; i < 60; i++) {
      snowParticlesRef.current.push({
        x: (Math.random() - 0.5) * 1600,
        y: (Math.random() - 0.5) * 1000,
        speed: Math.random() * 100 + 60,
        size: Math.random() * 3 + 2,
      });
    }
    print('❄️ [LevelIntro] 开场动画开始');
  }

  const handleUpdate = (deltaTime: number) => {
    if (completedRef.current) {
      return false;
    }

    timerRef.current += deltaTime;

    const spacePressed = Keyboard.isKeyPressed(KeyName.Space);
    if (!lastSpacePressedRef.current && spacePressed) {
      print('⏭️ [LevelIntro] 跳过开场动画');
      completedRef.current = true;
      props.onComplete();
      return false;
    }
    lastSpacePressedRef.current = spacePressed;

    if (timerRef.current >= 3.5) {
      print('✅ [LevelIntro] 开场动画完成');
      completedRef.current = true;
      props.onComplete();
      return false;
    }

    return false;
  };

  const handleTapBegan = (touch: any) => {
    if (!completedRef.current) {
      print('⏭️ [LevelIntro] 点击跳过开场动画');
      completedRef.current = true;
      props.onComplete();
    }
    return true;
  };

  const timer = timerRef.current;
  let fadeOpacity = 1.0;
  if (timer < 0.5) {
    fadeOpacity = timer / 0.5;
  } else if (timer > 3.0) {
    fadeOpacity = (3.5 - timer) / 0.5;
  }

  const snowParticles = snowParticlesRef.current;

  return (
    <node onUpdate={handleUpdate} touchEnabled={true} onTapBegan={handleTapBegan} order={2000}>
      <draw-node>
        <rect-shape width={2000} height={2000} fillColor={0x000000ff} />
      </draw-node>

      <node opacity={fadeOpacity}>
        {snowParticles.map((particle, index) => (
          <SnowParticle
            key={`snow${index}`}
            initialX={particle.x}
            initialY={particle.y}
            speed={particle.speed}
            size={particle.size}
          />
        ))}
      </node>

      <node y={50} opacity={fadeOpacity}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={56}
          text={props.title}
          y={-14}
          alignment='Center'
        />
      </node>

      <node y={-20} opacity={fadeOpacity}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={32}
          text={props.subtitle}
          y={-8}
          alignment='Center'
        />
      </node>

      <node y={-280} opacity={fadeOpacity * 0.6}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={20}
          text="按空格或点击跳过"
          y={-5}
          alignment='Center'
        />
      </node>
    </node>
  );
};

// ===============================
// 10. SimpleDialog 组件
// ===============================
interface SimpleDialogProps {
  onComplete: () => void;
  messages: DialogMessage[];
  gameStateRef: any;
}

const SimpleDialog = (props: SimpleDialogProps) => {
  const lastSpacePressedRef = useRef(false);
  const initRef = useRef(false);
  const initializedDisplayRef = useRef(false);
  const completedRef = useRef(false);
  const currentIndexRef = useRef(0);
  const textLabelRef = useRef(null);
  const nameLabelRef = useRef(null);
  const avatarPlayerRef = useRef(null);
  const avatarNpcRef = useRef(null);
  const nameNodeRef = useRef(null);
  const avatarNodeRef = useRef(null);
  const activationDelayRef = useRef(0);
  const lastGameStateRef = useRef<string>('');

  if (!initRef.current) {
    print('💬 [SimpleDialog] 对话系统初始化');
    initRef.current = true;
  }

  const updateDisplay = () => {
    const msg = props.messages[currentIndexRef.current];
    if (!msg) return;

    const textLabel = textLabelRef.current;
    const nameLabel = nameLabelRef.current;
    const avatarPlayer = avatarPlayerRef.current;
    const avatarNpc = avatarNpcRef.current;
    const nameNode = nameNodeRef.current;
    const avatarNode = avatarNodeRef.current;

    if (textLabel) textLabel.text = msg.text;
    if (nameLabel) nameLabel.text = msg.name;
    // 切换头像显示
    if (avatarPlayer) avatarPlayer.visible = msg.speaker === 'player';
    if (avatarNpc) avatarNpc.visible = msg.speaker !== 'player';
    if (nameNode) {
      nameNode.x = msg.speaker === 'player' ? -350 : 500;
    }
    if (avatarNode) {
      avatarNode.x = msg.speaker === 'player' ? -380 : 380;
    }

    print(`📖 [SimpleDialog] 显示: ${msg.name}: ${msg.text}`);
  };

  const handleNext = () => {
    if (completedRef.current) {
      return;
    }

    print(`📖 [SimpleDialog] 当前索引: ${currentIndexRef.current}, 总消息数: ${props.messages.length}`);

    currentIndexRef.current++;

    if (currentIndexRef.current >= props.messages.length) {
      print('✅ [SimpleDialog] 对话完成');
      completedRef.current = true;
      props.onComplete();
    } else {
      print(`📖 [SimpleDialog] 切换到第 ${currentIndexRef.current + 1} 句`);
      updateDisplay();
    }
  };

  const handleUpdate = (deltaTime: number) => {
    const currentGameState = props.gameStateRef.current;

    if (lastGameStateRef.current !== 'dialog' && currentGameState === 'dialog') {
      print('🔄 [SimpleDialog] 重置对话状态，开始新对话');
      completedRef.current = false;
      currentIndexRef.current = 0;
      initializedDisplayRef.current = false;
      activationDelayRef.current = 0;
      lastSpacePressedRef.current = false;
    }

    lastGameStateRef.current = currentGameState;

    if (completedRef.current) {
      return false;
    }

    if (currentGameState !== 'dialog') {
      lastSpacePressedRef.current = false;
      activationDelayRef.current = 0;
      return false;
    }

    // 激活延迟，防止按E的同时误触发空格
    if (activationDelayRef.current < 0.2) {
      activationDelayRef.current += deltaTime;
      return false;
    }

    // 初始化显示第一句对话
    if (!initializedDisplayRef.current && textLabelRef.current) {
      updateDisplay();
      initializedDisplayRef.current = true;
      lastSpacePressedRef.current = Keyboard.isKeyPressed(KeyName.Space);
      return false;  // 初始化显示后直接返回，不检测空格
    }

    const spacePressed = Keyboard.isKeyPressed(KeyName.Space);

    if (!lastSpacePressedRef.current && spacePressed) {
      handleNext();
    }

    lastSpacePressedRef.current = spacePressed;

    return false;
  };

  let currentMsg = props.messages[currentIndexRef.current];
  if (!currentMsg) {
    currentMsg = props.messages[props.messages.length - 1] || { speaker: 'npc', name: '...', text: '...' };
  }

  const isPlayer = currentMsg.speaker === 'player';

  return (
    <node x={0} y={0} onUpdate={handleUpdate} touchEnabled={false} order={1000} scaleX={1} scaleY={1}>
      <node y={-250}>
        <sprite /*file={'Image/ui_panel.png'}*/ width={900} height={250} opacity={0.9} />  {/* 对话框面板背景 */}

        <draw-node>
          <polygon-shape
            verts={[
              Vec2(0, -8),
              Vec2(10, 0),
              Vec2(0, 8)
            ]}
            x={420}
            y={100}
            fillColor={0xffffffff}
          />
        </draw-node>

        <node ref={nameNodeRef} x={isPlayer ? -350 : 500} y={-95}>
          <sprite /*file={'Image/ui_button.png'}*/ width={200} height={40} />  {/* 名字标签背景 */}

          <label
            ref={nameLabelRef}
            fontName='sarasa-mono-sc-regular'
            fontSize={24}
            text={currentMsg.name}
            x={-80}
            y={-5}
            alignment='Center'
          />
        </node>

        <node ref={avatarNodeRef} x={isPlayer ? -380 : 380} y={0}>
          <sprite
            ref={avatarPlayerRef}
            file={'Image/avatar_player.png'}
            width={120}
            height={120}
            filter="Point"
            visible={isPlayer}
          />
          <sprite
            ref={avatarNpcRef}
            file={'Image/avatar_npc.png'}
            width={120}
            height={120}
            filter="Point"
            visible={!isPlayer}
          />
        </node>

        <node x={0} y={0}>
          <label
            ref={textLabelRef}
            fontName='sarasa-mono-sc-regular'
            fontSize={28}
            text={currentMsg.text}
            width={600}
            height={120}
            x={0}
            y={20}
            alignment='Center'
          />
        </node>

        <node y={-140}>
          <draw-node>
            {props.messages.map((_, index) => {
              const radius = 4;
              const color = index <= currentIndexRef.current ? 0xffffffff : 0x66666666;
              const spacing = 18;
              const startX = -(props.messages.length - 1) * spacing / 2;
              const centerX = startX + index * spacing;

              const points = [];
              for (let i = 0; i < 16; i++) {
                const angle = (i / 16) * Math.PI * 2;
                points.push(Vec2(
                  Math.cos(angle) * radius + centerX,
                  Math.sin(angle) * radius
                ));
              }

              return (
                <polygon-shape
                  key={`dot${index}`}
                  verts={points}
                  fillColor={color}
                />
              );
            })}
          </draw-node>
        </node>

        <node y={-100}>
          <label
            fontName='sarasa-mono-sc-regular'
            fontSize={18}
            text="按空格继续"
            alignment='Center'
            opacity={0.7}
          />
        </node>
      </node>
    </node>
  );
};

// ===============================
// 11. 菜单与游戏场景
// ===============================
const startGame = (levelIndex: number) => {
  print('🎮 开始关卡');
  currentLevel = levelIndex;
  playerPosition = {
    x: LEVELS[currentLevel].playerStart.x,
    y: LEVELS[currentLevel].playerStart.y
  };

  const entry = Director.entry;
  if (entry) {
    entry.removeAllChildren();
    // 只有第1关显示开场动画
    
      entry.addChild(toNode(
        <ScaledRoot>
          <Game />
        </ScaledRoot>
      ));
    
  }
};

const LevelIntroScene = (props: { levelIndex: number }) => {
  const completedRef = useRef(false);

  const handleIntroComplete = () => {
    if (completedRef.current) return;
    completedRef.current = true;

    print('🎬 [LevelIntroScene] 开场动画完成，进入游戏');
    const entry = Director.entry;
    if (entry) {
      entry.removeAllChildren();
      entry.addChild(toNode(
        <ScaledRoot>
          <Game />
        </ScaledRoot>
      ));
    }
  };

  const level = LEVELS[props.levelIndex];

  return (
    <LevelIntro
      title={level.intro.title}
      subtitle={level.intro.subtitle}
      onComplete={handleIntroComplete}
    />
  );
};

const returnToMenu = () => {
  print('📋 返回主菜单');
  const entry = Director.entry;
  if (entry) {
    entry.removeAllChildren();
    entry.addChild(toNode(
      <ScaledRoot>
        <MainMenu />
      </ScaledRoot>
    ));
  }
};

// ===============================
// 12. 主菜单
// ===============================
const MainMenu = () => {
  const difficultyLabelRef = useRef(null);
  const bgmStartedRef = useRef(false);
  const confirmDialogRef = useRef(null);
  const showConfirmRef = useRef(false);

  // 播放菜单音乐
  if (!bgmStartedRef.current) {
    playBGM(SOUND_FILES.bgm_menu, 0.4);
    bgmStartedRef.current = true;
  }

  const handleMenuTap = (touch: any) => {
    const loc = touch.location;
    const clickX = loc.x;
    const clickY = loc.y;

    print(`🖱️ [主菜单] 点击位置: (${clickX}, ${clickY})`);

    // 如果确认对话框显示中，处理确认/取消
    if (showConfirmRef.current) {
      // 确认按钮 x=-70
      if (clickX >= -120 && clickX <= -20 && clickY >= -100 && clickY <= -40) {
        print('✅ [主菜单] 确认清空存档');
        clearSave();
        showConfirmRef.current = false;
        const dialog = confirmDialogRef.current;
        if (dialog) dialog.visible = false;
        return true;
      }
      // 取消按钮 x=70
      if (clickX >= 20 && clickX <= 120 && clickY >= -100 && clickY <= -40) {
        print('❌ [主菜单] 取消清空存档');
        showConfirmRef.current = false;
        const dialog = confirmDialogRef.current;
        if (dialog) dialog.visible = false;
        return true;
      }
      return true;  // 阻止其他点击
    }

    // 按钮位置：左下角，x=-500，y从-200到100
    const BTN_X = -500;
    const BTN_HALF_W = 140;
    const BTN_HALF_H = 35;

    // 选关按钮 y=60
    if (
      clickX >= BTN_X - BTN_HALF_W && clickX <= BTN_X + BTN_HALF_W &&
      clickY >= 60 - BTN_HALF_H && clickY <= 60 + BTN_HALF_H
    ) {
      print('✅ [主菜单] 点击了选关');
      const entry = Director.entry;
      if (entry) {
        entry.removeAllChildren();
        entry.addChild(toNode(
          <ScaledRoot>
            <LevelSelectMenu />
          </ScaledRoot>
        ));
      }
      return true;
    }

    // 查看结局按钮 y=-20
    if (
      clickX >= BTN_X - BTN_HALF_W && clickX <= BTN_X + BTN_HALF_W &&
      clickY >= -20 - BTN_HALF_H && clickY <= -20 + BTN_HALF_H
    ) {
      print('🎬 [主菜单] 点击了查看结局');
      const entry = Director.entry;
      if (entry) {
        entry.removeAllChildren();
        entry.addChild(toNode(
          <ScaledRoot>
            <EndingGallery />
          </ScaledRoot>
        ));
      }
      return true;
    }

    // 难度选择按钮 y=-100
    if (
      clickX >= BTN_X - BTN_HALF_W - 100 && clickX <= BTN_X + BTN_HALF_W + 100 &&
      clickY >= -100 - BTN_HALF_H && clickY <= -100 + BTN_HALF_H
    ) {
      print('🎮 [主菜单] 点击了难度选择');
      cycleDifficulty();
      // 更新难度显示
      const label = difficultyLabelRef.current;
      if (label) {
        label.text = `难度: ${getDifficultyName()} (冲刺cd：${SPRINT_COOLDOWN}秒)`;
      }
      return true;
    }

    // 清空存档按钮 y=-180
    if (
      clickX >= BTN_X - BTN_HALF_W && clickX <= BTN_X + BTN_HALF_W &&
      clickY >= -180 - BTN_HALF_H && clickY <= -180 + BTN_HALF_H
    ) {
      print('🗑️ [主菜单] 点击了清空存档，显示确认对话框');
      showConfirmRef.current = true;
      const dialog = confirmDialogRef.current;
      if (dialog) dialog.visible = true;
      return true;
    }

    // 退出按钮 y=-260
    if (
      clickX >= BTN_X - BTN_HALF_W && clickX <= BTN_X + BTN_HALF_W &&
      clickY >= -260 - BTN_HALF_H && clickY <= -260 + BTN_HALF_H
    ) {
      print('👋 [主菜单] 退出游戏');
      App.shutdown();
      return true;
    }

    print('❌ [主菜单] 没有点击到任何按钮');
    return false;
  };

  return (
    <node touchEnabled={true} onTapBegan={handleMenuTap}>
      <sprite file={'Image/GameStart.png'} width={1950} height={1100} />  {/* 菜单背景图 */}

      <draw-node>
        <rect-shape width={2000} height={2000} fillColor={0x00000001} />
      </draw-node>

      {/* 选关按钮 */}
      <node x={-500} y={60}>
        <sprite /*file={'Image/ui_button.png'}*/ width={280} height={70} filter="Point" />
        <label fontName='sarasa-mono-sc-regular' fontSize={36} text="选关" y={-9} alignment='Center' />
      </node>

      {/* 查看结局按钮 */}
      <node x={-500} y={-20}>
        <sprite /*file={'Image/ui_button.png'}*/ width={280} height={70} filter="Point" />
        <label fontName='sarasa-mono-sc-regular' fontSize={36} text={`结局 (${getUnlockedEndingsCount()}/3)`} y={-9} alignment='Center' />
      </node>

      {/* 难度选择按钮 */}
      <node x={-500} y={-100}>
        <sprite /*file={'Image/ui_button.png'}*/ width={480} height={70} filter="Point" />
        <label
          ref={difficultyLabelRef}
          fontName='sarasa-mono-sc-regular'
          fontSize={32}
          text={`难度: ${getDifficultyName()} (冲刺cd：${SPRINT_COOLDOWN}秒)`}
          y={-8}
          alignment='Center'
        />
      </node>

      {/* 清空存档按钮 */}
      <node x={-500} y={-180}>
        <sprite /*file={'Image/ui_button.png'}*/ width={280} height={70} filter="Point" />
        <label fontName='sarasa-mono-sc-regular' fontSize={36} text="清空存档" y={-9} alignment='Center' />
      </node>

      {/* 退出按钮 */}
      <node x={-500} y={-260}>
        <sprite /*file={'Image/ui_button.png'}*/ width={280} height={70} filter="Point" />
        <label fontName='sarasa-mono-sc-regular' fontSize={36} text="退出" y={-9} alignment='Center' />
      </node>

      {/* 清空存档确认对话框 */}
      <node ref={confirmDialogRef} visible={false} order={100}>
        <draw-node>
          <rect-shape width={2000} height={2000} fillColor={0x000000cc} />
        </draw-node>
        <node y={50}>
          <label fontName='sarasa-mono-sc-regular' fontSize={36} text="确定要清空存档吗？" alignment='Center' />
        </node>
        <node y={0}>
          <label fontName='sarasa-mono-sc-regular' fontSize={24} text="所有进度将被删除" alignment='Center' opacity={0.7} />
        </node>
        {/* 确认按钮 */}
        <node x={-70} y={-70}>
          <sprite /*file={'Image/ui_button.png'}*/ width={100} height={50} filter="Point" />
          <label fontName='sarasa-mono-sc-regular' fontSize={28} text="确定" y={-7} alignment='Center' />
        </node>
        {/* 取消按钮 */}
        <node x={70} y={-70}>
          <sprite /*file={'Image/ui_button.png'}*/ width={100} height={50} filter="Point" />
          <label fontName='sarasa-mono-sc-regular' fontSize={28} text="取消" y={-7} alignment='Center' />
        </node>
      </node>
    </node>
  );
};

// ===============================
// 13. 结局画廊
// ===============================
// 结局图片配置
const ENDING_IMAGES = {
  fake: [
    'Image/cg/LostEnding-1.png', 'Image/cg/LostEnding-2.png', 'Image/cg/LostEnding-3.png', 'Image/cg/LostEnding-4.png',
    'Image/cg/LostEnding-5.png', 'Image/cg/LostEnding-6.png', 'Image/cg/LostEnding-7.png', 'Image/cg/LostEnding-8.png'
  ],  // 假结局CG图片
  sacrifice: [
    'Image/cg/SacrificeEnding-1.png', 'Image/cg/SacrificeEnding-2.png', 'Image/cg/SacrificeEnding-3.png', 'Image/cg/SacrificeEnding-4.png',
    'Image/cg/SacrificeEnding-5.png', 'Image/cg/SacrificeEnding-6.png', 'Image/cg/SacrificeEnding-7.png', 'Image/cg/SacrificeEnding-8.png'
  ],  // 牺牲结局CG图片
  perfect: [
    'Image/cg/TrueEnding-1.png', 'Image/cg/TrueEnding-2.png', 'Image/cg/TrueEnding-3.png', 'Image/cg/TrueEnding-4.png',
    'Image/cg/TrueEnding-5.png', 'Image/cg/TrueEnding-6.png', 'Image/cg/TrueEnding-7.png', 'Image/cg/TrueEnding-8.png'
  ],  // 完美结局CG图片
};

const EndingGallery = () => {
  const viewingEndingRef = useRef<string | null>(null);
  const imageIndexRef = useRef(0);
  const lastSpacePressedRef = useRef(false);
  const imageNodeRef = useRef(null);
  const pageHintRef = useRef(null);

  // 预创建所有sprite的ref - 假结局8张
  const fakeSprite0Ref = useRef(null);
  const fakeSprite1Ref = useRef(null);
  const fakeSprite2Ref = useRef(null);
  const fakeSprite3Ref = useRef(null);
  const fakeSprite4Ref = useRef(null);
  const fakeSprite5Ref = useRef(null);
  const fakeSprite6Ref = useRef(null);
  const fakeSprite7Ref = useRef(null);
  // 牺牲结局8张
  const sacrificeSprite0Ref = useRef(null);
  const sacrificeSprite1Ref = useRef(null);
  const sacrificeSprite2Ref = useRef(null);
  const sacrificeSprite3Ref = useRef(null);
  const sacrificeSprite4Ref = useRef(null);
  const sacrificeSprite5Ref = useRef(null);
  const sacrificeSprite6Ref = useRef(null);
  const sacrificeSprite7Ref = useRef(null);
  // 完美结局8张
  const perfectSprite0Ref = useRef(null);
  const perfectSprite1Ref = useRef(null);
  const perfectSprite2Ref = useRef(null);
  const perfectSprite3Ref = useRef(null);
  const perfectSprite4Ref = useRef(null);
  const perfectSprite5Ref = useRef(null);
  const perfectSprite6Ref = useRef(null);
  const perfectSprite7Ref = useRef(null);

  const getAllSpriteRefs = () => [
    fakeSprite0Ref, fakeSprite1Ref, fakeSprite2Ref, fakeSprite3Ref, fakeSprite4Ref, fakeSprite5Ref, fakeSprite6Ref, fakeSprite7Ref,
    sacrificeSprite0Ref, sacrificeSprite1Ref, sacrificeSprite2Ref, sacrificeSprite3Ref, sacrificeSprite4Ref, sacrificeSprite5Ref, sacrificeSprite6Ref, sacrificeSprite7Ref,
    perfectSprite0Ref, perfectSprite1Ref, perfectSprite2Ref, perfectSprite3Ref, perfectSprite4Ref, perfectSprite5Ref, perfectSprite6Ref, perfectSprite7Ref
  ];

  const getSpriteRef = (ending: string, index: number) => {
    const fakeRefs = [fakeSprite0Ref, fakeSprite1Ref, fakeSprite2Ref, fakeSprite3Ref, fakeSprite4Ref, fakeSprite5Ref, fakeSprite6Ref, fakeSprite7Ref];
    const sacrificeRefs = [sacrificeSprite0Ref, sacrificeSprite1Ref, sacrificeSprite2Ref, sacrificeSprite3Ref, sacrificeSprite4Ref, sacrificeSprite5Ref, sacrificeSprite6Ref, sacrificeSprite7Ref];
    const perfectRefs = [perfectSprite0Ref, perfectSprite1Ref, perfectSprite2Ref, perfectSprite3Ref, perfectSprite4Ref, perfectSprite5Ref, perfectSprite6Ref, perfectSprite7Ref];

    if (ending === 'fake' && index < 8) return fakeRefs[index];
    if (ending === 'sacrifice' && index < 8) return sacrificeRefs[index];
    if (ending === 'perfect' && index < 8) return perfectRefs[index];
    return null;
  };

  const hideAllSprites = () => {
    getAllSpriteRefs().forEach(ref => {
      if (ref.current) ref.current.visible = false;
    });
  };

  const showCurrentSprite = () => {
    hideAllSprites();
    const ending = viewingEndingRef.current;
    if (!ending) return;

    const spriteRef = getSpriteRef(ending, imageIndexRef.current);
    if (spriteRef && spriteRef.current) {
      spriteRef.current.visible = true;
    }

    const pageHint = pageHintRef.current;
    if (pageHint) {
      pageHint.text = `${imageIndexRef.current + 1} / 8`;
    }
  };

  const handleTap = (touch: any) => {
    const loc = touch.location;
    const clickX = loc.x;
    const clickY = loc.y;

    // 如果正在查看结局图片，点击继续下一张
    if (viewingEndingRef.current) {
      imageIndexRef.current++;
      if (imageIndexRef.current >= 8) {
        // 最后一张图片后返回画廊
        viewingEndingRef.current = null;
        imageIndexRef.current = 0;
        hideAllSprites();
        const imageNode = imageNodeRef.current;
        if (imageNode) imageNode.visible = false;
      } else {
        showCurrentSprite();
      }
      return true;
    }

    print(`🖱️ [结局画廊] 点击位置: (${clickX}, ${clickY})`);

    const Y_POS = 50;

    // 假结局
    if (clickX >= -250 && clickX <= -50 && clickY >= Y_POS - 75 && clickY <= Y_POS + 75) {
      if (unlockedEndings.fake) {
        print('🎬 [结局画廊] 查看假结局');
        viewingEndingRef.current = 'fake';
        imageIndexRef.current = 0;
        const imageNode = imageNodeRef.current;
        if (imageNode) imageNode.visible = true;
        showCurrentSprite();
      }
      return true;
    }

    // 牺牲结局
    if (clickX >= -100 && clickX <= 100 && clickY >= Y_POS - 75 && clickY <= Y_POS + 75) {
      if (unlockedEndings.sacrifice) {
        print('🎬 [结局画廊] 查看牺牲结局');
        viewingEndingRef.current = 'sacrifice';
        imageIndexRef.current = 0;
        const imageNode = imageNodeRef.current;
        if (imageNode) imageNode.visible = true;
        showCurrentSprite();
      }
      return true;
    }

    // 完美结局
    if (clickX >= 50 && clickX <= 250 && clickY >= Y_POS - 75 && clickY <= Y_POS + 75) {
      if (unlockedEndings.perfect) {
        print('🎬 [结局画廊] 查看完美结局');
        viewingEndingRef.current = 'perfect';
        imageIndexRef.current = 0;
        const imageNode = imageNodeRef.current;
        if (imageNode) imageNode.visible = true;
        showCurrentSprite();
      }
      return true;
    }

    // 返回按钮
    if (clickX >= -100 && clickX <= 100 && clickY >= -150 && clickY <= -90) {
      print('🔙 [结局画廊] 返回主菜单');
      returnToMenu();
      return true;
    }

    return false;
  };

  const handleUpdate = (deltaTime: number) => {
    // 处理空格键切换图片
    if (viewingEndingRef.current) {
      const spacePressed = Keyboard.isKeyPressed(KeyName.Space);
      const spaceDown = !lastSpacePressedRef.current && spacePressed;
      lastSpacePressedRef.current = spacePressed;

      if (spaceDown) {
        imageIndexRef.current++;

        if (imageIndexRef.current >= 8) {
          // 最后一张图片后返回画廊
          viewingEndingRef.current = null;
          imageIndexRef.current = 0;
          hideAllSprites();
          const imageNode = imageNodeRef.current;
          if (imageNode) imageNode.visible = false;
        } else {
          showCurrentSprite();
        }
      }
    } else {
      lastSpacePressedRef.current = Keyboard.isKeyPressed(KeyName.Space);
    }

    return false;
  };

  return (
    <node touchEnabled={true} onTapBegan={handleTap} onUpdate={handleUpdate}>
      <sprite file={'Image/menu_bg.png'} width={1950} height={1100} />  {/* 结局画廊背景 */}
      <draw-node>
        <rect-shape width={2000} height={2000} fillColor={0x00000001} />
      </draw-node>

      {/* 标题 */}
      <node y={200}>
        <label fontName='sarasa-mono-sc-regular' fontSize={40} text="结局画廊" y={-10} alignment='Center' />
      </node>

      {/* 假结局 */}
      <node x={-150} y={50}>
        <sprite
          /*file={'Image/ui_button.png'}   结局选择按钮背景 */
          width={200}
          height={150}
          filter="Point"
          opacity={unlockedEndings.fake ? 1.0 : 0.3}
        />
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={24}
          text={unlockedEndings.fake ? "谎言" : "???"}
          y={-6}
          alignment='Center'
        />
      </node>

      {/* 牺牲结局 */}
      <node x={0} y={50}>
        <sprite
          /* file={'Image/ui_button.png'}   结局选择按钮背景 */
          width={200}
          height={150}
          filter="Point"
          opacity={unlockedEndings.sacrifice ? 1.0 : 0.3}
        />
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={24}
          text={unlockedEndings.sacrifice ? "交换" : "???"}
          y={-6}
          alignment='Center'
        />
      </node>

      {/* 完美结局 */}
      <node x={150} y={50}>
        <sprite
         /* file={'Image/ui_button.png'}  结局选择按钮背景 */
          width={200}
          height={150}
          filter="Point"
          opacity={unlockedEndings.perfect ? 1.0 : 0.3}
        />
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={24}
          text={unlockedEndings.perfect ? "回家" : "???"}
          y={-6}
          alignment='Center'
        />
      </node>

      {/* 返回按钮 */}
      <node y={-120}>
        <sprite /*file={'Image/ui_button.png'}*/ width={200} height={60} filter="Point" />  {/* 返回按钮背景 */}
        <label fontName='sarasa-mono-sc-regular' fontSize={28} text="返回" y={-7} alignment='Center' />
      </node>

      {/* 结局图片查看 - 预创建所有sprite，通过显示/隐藏切换 */}
      <node ref={imageNodeRef} visible={false} order={100}>
        <draw-node>
          <rect-shape width={2000} height={2000} fillColor={0x000000dd} />
        </draw-node>
        {/* 假结局图片 - 预创建8张 */}
        <sprite ref={fakeSprite0Ref} file={ENDING_IMAGES.fake[0]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite1Ref} file={ENDING_IMAGES.fake[1]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite2Ref} file={ENDING_IMAGES.fake[2]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite3Ref} file={ENDING_IMAGES.fake[3]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite4Ref} file={ENDING_IMAGES.fake[4]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite5Ref} file={ENDING_IMAGES.fake[5]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite6Ref} file={ENDING_IMAGES.fake[6]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={fakeSprite7Ref} file={ENDING_IMAGES.fake[7]} width={1000} height={1000} filter="Point" visible={false} />
        {/* 牺牲结局图片 - 预创建8张 */}
        <sprite ref={sacrificeSprite0Ref} file={ENDING_IMAGES.sacrifice[0]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite1Ref} file={ENDING_IMAGES.sacrifice[1]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite2Ref} file={ENDING_IMAGES.sacrifice[2]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite3Ref} file={ENDING_IMAGES.sacrifice[3]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite4Ref} file={ENDING_IMAGES.sacrifice[4]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite5Ref} file={ENDING_IMAGES.sacrifice[5]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite6Ref} file={ENDING_IMAGES.sacrifice[6]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={sacrificeSprite7Ref} file={ENDING_IMAGES.sacrifice[7]} width={1000} height={1000} filter="Point" visible={false} />
        {/* 完美结局图片 - 预创建8张 */}
        <sprite ref={perfectSprite0Ref} file={ENDING_IMAGES.perfect[0]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite1Ref} file={ENDING_IMAGES.perfect[1]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite2Ref} file={ENDING_IMAGES.perfect[2]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite3Ref} file={ENDING_IMAGES.perfect[3]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite4Ref} file={ENDING_IMAGES.perfect[4]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite5Ref} file={ENDING_IMAGES.perfect[5]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite6Ref} file={ENDING_IMAGES.perfect[6]} width={1000} height={1000} filter="Point" visible={false} />
        <sprite ref={perfectSprite7Ref} file={ENDING_IMAGES.perfect[7]} width={1000} height={1000} filter="Point" visible={false} />
        <node y={-350}>
          <label
            ref={pageHintRef}
            fontName='sarasa-mono-sc-regular'
            fontSize={20}
            text="1 / 8"
            alignment='Center'
            opacity={0.7}
          />
        </node>
        <node y={-380}>
          <label
            fontName='sarasa-mono-sc-regular'
            fontSize={18}
            text="按空格或点击继续"
            alignment='Center'
            opacity={0.5}
          />
        </node>
      </node>
    </node>
  );
};

// ===============================
// 13. 选关菜单
// ===============================
const LevelSelectMenu = () => {
  const selectedLevelRef = useRef<number | null>(null);

  const handleUpdate = (deltaTime: number) => {
    const selectedLevel = selectedLevelRef.current;
    if (selectedLevel !== null) {
      selectedLevelRef.current = null;
      startGame(selectedLevel);
    }
    return false;
  };

  const handleMenuTap = (touch: any) => {
    const loc = touch.location;
    const clickX = loc.x;
    const clickY = loc.y;

    print(`🖱️ [选关菜单] 点击位置: (${clickX}, ${clickY})`);

    const BTN_SIZE = 120;
    const BTN_HALF = 60;
    const TOP_ROW_Y = 120;
    const BOTTOM_ROW_Y = -40;
    const X_POSITIONS = [-225, -75, 75, 225];

    if (clickY >= TOP_ROW_Y - BTN_HALF && clickY <= TOP_ROW_Y + BTN_HALF) {
      for (let col = 0; col < 4; col++) {
        const btnX = X_POSITIONS[col];
        const levelIndex = col;

        if (
          clickX >= btnX - BTN_HALF &&
          clickX <= btnX + BTN_HALF &&
          levelIndex < LEVELS.length
        ) {
          // 排除隐藏关卡
          if (LEVELS[levelIndex].hidden) {
            return true;
          }

          if (!isLevelUnlocked(levelIndex)) {
            print(`🔒 [选关菜单] 第 ${levelIndex + 1} 关尚未解锁`);
            return true;
          }

          print(`✅ [选关菜单] 选择了第 ${levelIndex + 1} 关`);
          selectedLevelRef.current = levelIndex;
          return true;
        }
      }
    }

    if (clickY >= BOTTOM_ROW_Y - BTN_HALF && clickY <= BOTTOM_ROW_Y + BTN_HALF) {
      for (let col = 0; col < 4; col++) {
        const btnX = X_POSITIONS[col];
        const levelIndex = col + 4;

        if (
          clickX >= btnX - BTN_HALF &&
          clickX <= btnX + BTN_HALF
        ) {
          if (col === 3) {
            print('🔙 [选关菜单] 返回主菜单');
            returnToMenu();
            return true;
          }

          if (levelIndex < LEVELS.length) {
            // 排除隐藏关卡
            if (LEVELS[levelIndex].hidden) {
              return true;
            }

            if (!isLevelUnlocked(levelIndex)) {
              print(`🔒 [选关菜单] 第 ${levelIndex + 1} 关尚未解锁`);
              return true;
            }

            print(`✅ [选关菜单] 选择了第 ${levelIndex + 1} 关`);
            selectedLevelRef.current = levelIndex;
            return true;
          }
        }
      }
    }

    print('❌ [选关菜单] 没有点击到任何按钮');
    return false;
  };

  const renderLevelButton = (index: number, x: number, y: number) => {
    // 不显示隐藏关卡
    if (LEVELS[index] && LEVELS[index].hidden) {
      return null;
    }

    const isUnlocked = isLevelUnlocked(index);

    return (
      <node key={`btn${index}`} x={x} y={y}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={36}
          text={isUnlocked ? `${index + 1}` : `🔒`}
          y={-9}
          alignment='Center'
          opacity={isUnlocked ? 1.0 : 0.5}
        />
      </node>
    );
  };

  const renderBackButton = (x: number, y: number) => {
    return (
      <node key="btnBack" x={x} y={y}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={36}
          text="返回"
          y={-9}
          alignment='Center'
        />
      </node>
    );
  };

  const TOP_ROW_Y = 120;
  const BOTTOM_ROW_Y = -40;
  const X_POSITIONS = [-225, -75, 75, 225];

  return (
    <node onUpdate={handleUpdate} touchEnabled={true} onTapBegan={handleMenuTap}>
      <sprite file={'Image/menu_bg.png'} width={1950} height={1100} />  {/* 选关菜单背景 */}

      <draw-node>
        <rect-shape width={2000} height={2000} fillColor={0x00000001} />
      </draw-node>

      {[0, 1, 2, 3].map((col) => {
        const levelIndex = col;
        if (levelIndex < LEVELS.length) {
          return renderLevelButton(levelIndex, X_POSITIONS[col], TOP_ROW_Y);
        }
        return null;
      })}

      {[0, 1, 2, 3].map((col) => {
        const levelIndex = col + 4;

        if (col === 3) {
          return renderBackButton(X_POSITIONS[col], BOTTOM_ROW_Y);
        }

        if (levelIndex < LEVELS.length) {
          return renderLevelButton(levelIndex, X_POSITIONS[col], BOTTOM_ROW_Y);
        }

        return null;
      })}

      {/* 收藏品数量提示 */}
      <node y={-150}>
        <label
          fontName='sarasa-mono-sc-regular'
          fontSize={24}
          text={`记忆碎片: ${getCollectedCount()} / 7`}
          y={-6}
          alignment='Center'
          opacity={0.8}
        />
      </node>
    </node>
  );
};

const Game = () => {
  const worldRef = useRef(null);
  const debugWorldRef = useRef(null);  // 调试物理世界（只显示桥梁碰撞箱）
  const cameraRef = useRef(null);
  const backgroundRef = useRef(null);
  const shakeTimerRef = useRef(0);
  const gameStateRef = useRef<'playing' | 'dialog' | 'dead' | 'won' | 'transition'>('playing');
  const stateTimerRef = useRef(0);
  const playerCanMoveRef = useRef(true);
  const dialogNodeRef = useRef(null);
  const cameraInitRef = useRef(false);
  const hasMetNPCRef = useRef(false);
  const nearNPCRef = useRef(false);
  const promptNodeRef = useRef(null);
  const lastEPressedRef = useRef(false);
  const bgmStartedRef = useRef(false);
  const secretTargetLevelRef = useRef<number | null>(null);
  const triggerDeathRef = useRef(false);  // 用于触发玩家死亡动画

  // 播放背景音乐（仅一次）
  if (!bgmStartedRef.current) {
    const levelBGM = getLevelBGM(currentLevel);
    playBGM(levelBGM, 0.4);
    bgmStartedRef.current = true;
  }

  const handleDash = () => {
    shakeTimerRef.current = SHAKE_DURATION;
  };

  const handleDeath = () => {
    print('💀 [Game] 玩家死亡动画完成，重生');
    restartLevel();
  };

  const handleGoal = () => {
    print('🎉 [Game] 玩家到达终点!');
    playerCanMoveRef.current = false;

    const dialog = dialogNodeRef.current;
    if (dialog) {
      dialog.visible = false;
      dialog.touchEnabled = false;
    }

    // 第七关正常终点 - 假结局（女主消失）
    if (currentLevel === 6) {
      print('🎬 [Game] 第七关完成，显示假结局');
      gameStateRef.current = 'transition';
      const entry = Director.entry;
      if (entry) {
        const endingNode = toNode(
          <ScaledRoot>
            <EndingScene
              dialogType="fakeEnding"
              endingImages={[
                'Image/cg/LostEnding-1.png', 'Image/cg/LostEnding-2.png', 'Image/cg/LostEnding-3.png', 'Image/cg/LostEnding-4.png',
                'Image/cg/LostEnding-5.png', 'Image/cg/LostEnding-6.png', 'Image/cg/LostEnding-7.png', 'Image/cg/LostEnding-8.png'
              ]}
              onComplete={() => {
                print('✅ [Ending] 假结局完成');
                unlockedEndings.fake = true;
                unlockNextLevel(6);
                returnToMenu();
              }}
            />
          </ScaledRoot>
        );
        entry.addChild(endingNode);
      }
      return;
    }

    // 第八关终点 - 根据收集品显示不同结局
    if (currentLevel === 7) {
      print('🎬 [Game] 第八关完成，显示结局');
      gameStateRef.current = 'transition';
      const entry = Director.entry;
      if (entry) {
        const allCollected = isAllCollected();
        const collectedCount = getCollectedCount();
        print(`🌟 [Game] 收集品: ${collectedCount}/7, 全部收集: ${allCollected}`);

        const endingNode = toNode(
          <ScaledRoot>
            <EndingScene
              dialogType={allCollected ? "perfectEnding" : "sacrificeEnding"}
              endingImages={allCollected
                ? [
                    'Image/cg/TrueEnding-1.png', 'Image/cg/TrueEnding-2.png', 'Image/cg/TrueEnding-3.png', 'Image/cg/TrueEnding-4.png',
                    'Image/cg/TrueEnding-5.png', 'Image/cg/TrueEnding-6.png', 'Image/cg/TrueEnding-7.png', 'Image/cg/TrueEnding-8.png'
                  ]
                : [
                    'Image/cg/SacrificeEnding-1.png', 'Image/cg/SacrificeEnding-2.png', 'Image/cg/SacrificeEnding-3.png', 'Image/cg/SacrificeEnding-4.png',
                    'Image/cg/SacrificeEnding-5.png', 'Image/cg/SacrificeEnding-6.png', 'Image/cg/SacrificeEnding-7.png', 'Image/cg/SacrificeEnding-8.png'
                  ]}
              onComplete={() => {
                print('✅ [Ending] 结局完成');
                if (allCollected) {
                  unlockedEndings.perfect = true;
                } else {
                  unlockedEndings.sacrifice = true;
                }
                saveCollectibles();
                returnToMenu();
              }}
            />
          </ScaledRoot>
        );
        entry.addChild(endingNode);
      }
      return;
    }

    // 其他关卡正常处理
    print(`⏱️ [Game] 2秒后进入下一关...`);
    gameStateRef.current = 'won';
    stateTimerRef.current = 2.0;
  };

  const handleSecretGoal = () => {
    const secretGoal = level.secretGoal;
    if (!secretGoal) return;

    print('🌟 [Game] 玩家发现隐藏终点！跳转到第 8 关');
    gameStateRef.current = 'won';
    stateTimerRef.current = 1.5;
    playerCanMoveRef.current = false;
    secretTargetLevelRef.current = 7;

    const dialog = dialogNodeRef.current;
    if (dialog) {
      dialog.visible = false;
      dialog.touchEnabled = false;
    }
  };

  const handleDialogComplete = () => {
    print('✅ [Game] 对话完成,解锁玩家控制');
    gameStateRef.current = 'playing';
    playerCanMoveRef.current = true;

    const dialog = dialogNodeRef.current;
    if (dialog) {
      dialog.visible = false;
      dialog.touchEnabled = false;
    }
  };

  const restartLevel = () => {
    print(`🔄 [Game] 重生关卡 ${currentLevel + 1}`);
    const entry = Director.entry;
    if (entry) {
      entry.removeAllChildren();
      entry.addChild(toNode(
        <ScaledRoot>
          <Game />
        </ScaledRoot>
      ));
    }
  };

  const nextLevel = () => {
    print(`🏆 [Game] 完成第 ${currentLevel + 1} 关`);
    unlockNextLevel(currentLevel);

    if (currentLevel + 1 < LEVELS.length) {
      print(`🎮 [Game] 进入第 ${currentLevel + 2} 关`);
      startGame(currentLevel + 1);
    } else {
      print('🎊 所有关卡完成!');
      returnToMenu();
    }
  };

  const handleUpdate = (deltaTime: number) => {
    const camera = cameraRef.current;
    if (!camera) return false;

    if (!cameraInitRef.current) {
      const level = LEVELS[currentLevel];
      camera.x = -level.playerStart.x - 150;
      camera.y = -level.playerStart.y;
      cameraInitRef.current = true;
    }

    if (Keyboard.isKeyDown(KeyName.Escape)) {
      returnToMenu();
      return false;
    }

    const targetX = playerPosition.x;
    const targetY = playerPosition.y;

    let shakeX = 0;
    let shakeY = 0;

    if (shakeTimerRef.current > 0) {
      shakeTimerRef.current -= deltaTime;
      const intensity = SHAKE_INTENSITY * (shakeTimerRef.current / SHAKE_DURATION);
      shakeX = (Math.random() - 0.5) * 2 * intensity;
      shakeY = (Math.random() - 0.5) * 2 * intensity;
    }

    let cameraX = -targetX + shakeX;
    camera.y = -targetY + shakeY;

    // 应用摄像机X轴限制
    const level = LEVELS[currentLevel];
    if (level.cameraLimitX) {
      cameraX = Math.max(level.cameraLimitX.min, Math.min(level.cameraLimitX.max, cameraX));
    }
    camera.x = cameraX;

    // 更新背景位置，产生视差效果
    const background = backgroundRef.current;
    if (background) {
      const level = LEVELS[currentLevel];
      const parallax = level.background.parallax || 0.5;
      background.x = -targetX * parallax;
      background.y = -targetY * parallax;
    }

    if (gameStateRef.current === 'playing') {
      const level = LEVELS[currentLevel];
      const npcX = level.npc.x;
      const npcY = level.npc.y;
      const dx = playerPosition.x - npcX;
      const dy = playerPosition.y - npcY;
      const distance = Math.sqrt(dx * dx + dy * dy);

      const isNear = distance < 80;
      nearNPCRef.current = isNear;

      const prompt = promptNodeRef.current;
      if (prompt) {
        prompt.visible = isNear;
        if (isNear) {
          prompt.x = npcX;
          prompt.y = npcY + 60;
        }
      }

      const ePressed = Keyboard.isKeyPressed(KeyName.E);
      if (isNear && !lastEPressedRef.current && ePressed) {
        print('💬 [Game] 按E键触发对话');
        gameStateRef.current = 'dialog';
        playerCanMoveRef.current = false;

        const dialog = dialogNodeRef.current;
        if (dialog) {
          dialog.visible = true;
          dialog.touchEnabled = true;
        }

        if (prompt) {
          prompt.visible = false;
        }
      }
      lastEPressedRef.current = ePressed;
    }

    if (gameStateRef.current === 'won') {
      stateTimerRef.current -= deltaTime;
      if (stateTimerRef.current <= 0) {
        if (secretTargetLevelRef.current !== null) {
          // 跳转到隐藏关卡
          secretTargetLevelRef.current = null;
          startGame(7);
        } else {
          nextLevel();
        }
      }
    }

    return false;
  };

  const level = LEVELS[currentLevel];

  return (
    <>
      {/* 背景层 - 独立于相机，位置在handleUpdate中动态更新产生视差效果 */}
      <node ref={backgroundRef} order={-100}>
        {level.background.animated ? (
          <AnimatedBackground
            file={level.background.file}
            width={level.background.width}
            height={level.background.height}
            frameWidth={level.background.frameWidth || level.background.width}
            frameHeight={level.background.frameHeight || level.background.height}
            frames={level.background.frames || 1}
            duration={level.background.duration || 1.0}
            loop={true}
          />
        ) : (
          <sprite
            file={level.background.file}
            width={level.background.width}
            height={level.background.height}
            filter="Point"
          />
        )}
      </node>

      {/* 相机节点 - 包含游戏世界 */}
      <node ref={cameraRef} onUpdate={handleUpdate}>
        <physics-world ref={worldRef} showDebug={false} gravity={GRAVITY}>
          <Player
            x={level.playerStart.x}
            y={level.playerStart.y}
            worldRef={worldRef}
            debugWorldRef={debugWorldRef}
            onDash={handleDash}
            onDeath={handleDeath}
            onGoal={handleGoal}
            onSecretGoal={handleSecretGoal}
            levelConfig={level}
            canMoveRef={playerCanMoveRef}
            triggerDeathRef={triggerDeathRef}
          />

          {level.platforms.map((p, i) => (
            <Platform key={`p${i}`} x={p.x} y={p.y} w={p.w} h={p.h} file={p.file} />
          ))}

          {level.hazards.map((h, i) => (
            <Hazard key={`h${i}`} x={h.x} y={h.y} w={h.w} h={h.h} file={h.file} />
          ))}

          {level.movingSpikes && level.movingSpikes.map((spike, i) => (
            <MovingSpike
              key={`ms${i}`}
              startX={spike.startX}
              endX={spike.endX}
              y={spike.y}
              width={spike.width}
              height={spike.height}
              speed={spike.speed}
              delay={spike.delay}
              file={spike.file}
              onPlayerHit={() => {
                triggerDeathRef.current = true;
              }}
            />
          ))}

          <Goal x={level.goal.x} y={level.goal.y} size={level.goal.size} />

          {level.secretGoal && (
            <SecretGoal x={level.secretGoal.x} y={level.secretGoal.y} size={level.secretGoal.size} />
          )}

          <NPC x={level.npc.x} y={level.npc.y} />

          {level.collectible && (
            <Collectible
              x={level.collectible.x}
              y={level.collectible.y}
              levelIndex={currentLevel}
              file={level.collectible.file}
              onCollect={() => {
                print(`✨ [Game] 收集品已收集！当前进度: ${getCollectedCount()}/7`);
                saveCollectibles();
              }}
            />
          )}

          {/* 关卡内文字提示 */}
          {level.texts && level.texts.map((textConfig, i) => (
            <LevelText
              key={`text${i}`}
              x={textConfig.x}
              y={textConfig.y}
              text={textConfig.text}
              fontSize={textConfig.fontSize}
              color={textConfig.color}
              showDistance={textConfig.showDistance}
            />
          ))}

          <node ref={promptNodeRef} visible={false} order={100}>
            <draw-node>
              <rect-shape
                width={180}
                height={50}
                fillColor={0x000000cc}
                borderColor={0xffffffff}
                borderWidth={2}
              />
            </draw-node>

            <label
              fontName='sarasa-mono-sc-regular'
              fontSize={28}
              text="按 E 对话"
              y={-7}
              alignment='Center'
            />
          </node>
        </physics-world>

        {/* 调试物理世界 - 只显示桥梁碰撞箱 */}
        <physics-world ref={debugWorldRef} showDebug={true} gravity={Vec2(0, 0)}>
        </physics-world>

        <node x={-600} y={400}>
          <sprite /*file={'Image/status_bar_bg.png'}*/ width={200} height={50} opacity={0.7} />  {/* 状态栏背景 */}
        </node>
      </node>

      {/* 对话框 - 独立于相机，固定在屏幕正上方 */}
      <node ref={dialogNodeRef} visible={false} order={2000}>
        <SimpleDialog
          messages={level.dialog.start}
          onComplete={handleDialogComplete}
          gameStateRef={gameStateRef}
        />
      </node>
    </>
  );
};

// ===============================
// 14. 启动游戏
// ===============================
// 窗口自适应设置
const DESIGN_WIDTH = 1500;  // 设计分辨率宽度
const DESIGN_HEIGHT = 720;  // 设计分辨率高度

// 计算缩放比例的函数
const calculateScale = () => {
  const actualWidth = View.size.width;
  const actualHeight = View.size.height;
  const scaleX = actualWidth / DESIGN_WIDTH;
  const scaleY = actualHeight / DESIGN_HEIGHT;
  const scale = Math.min(scaleX, scaleY);
  return Math.min(scale, 1);  // 最大缩放限制为1.2
};

// 全局缩放变量（初始值）
let globalScale = calculateScale();

// 上次窗口尺寸（用于检测变化）
let lastWindowWidth = View.size.width;
let lastWindowHeight = View.size.height;

print(`📺 [窗口] 设计分辨率: ${DESIGN_WIDTH}x${DESIGN_HEIGHT}`);
print(`📺 [窗口] 实际窗口: ${lastWindowWidth}x${lastWindowHeight}`);
print(`📺 [窗口] 缩放比例: ${globalScale.toFixed(2)}`);

// 动态缩放容器组件 - 自动响应窗口大小变化
const ScaledRoot = (props: { children: any }) => {
  const nodeRef = useRef(null);

  const handleUpdate = (deltaTime: number) => {
    const currentWidth = View.size.width;
    const currentHeight = View.size.height;

    // 检测窗口尺寸是否改变
    if (currentWidth !== lastWindowWidth || currentHeight !== lastWindowHeight) {
      lastWindowWidth = currentWidth;
      lastWindowHeight = currentHeight;
      globalScale = calculateScale();

      // 更新节点缩放
      const node = nodeRef.current;
      if (node) {
        node.scaleX = globalScale;
        node.scaleY = globalScale;
      }

      print(`📺 [窗口] 尺寸改变: ${currentWidth}x${currentHeight}, 缩放: ${globalScale.toFixed(2)}`);
    }

    return false;
  };

  return (
    <node ref={nodeRef} scaleX={globalScale} scaleY={globalScale} onUpdate={handleUpdate}>
      {props.children}
    </node>
  );
};

// 加载存档（包括收藏品进度）
loadSave();
toNode(
  <ScaledRoot>
    <MainMenu />
  </ScaledRoot>
);
